Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
54a0be4836 | ||
|
|
ea283dd490 | ||
|
|
0d67a4eb25 | ||
|
|
f93065a377 | ||
|
|
91f6cd1aa2 | ||
|
|
e3f585222a | ||
|
|
e8e771aaf2 | ||
|
|
1a89020243 | ||
|
|
e92a3d364f | ||
|
|
1a714f6deb | ||
|
|
c4c470c073 | ||
|
|
694cc55eaa |
@@ -15,11 +15,13 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: '24'
|
node-version: '26'
|
||||||
cache: npm
|
cache: npm
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
@@ -28,6 +30,48 @@ jobs:
|
|||||||
- name: Clean dist
|
- name: Clean dist
|
||||||
run: rm -f dist/*.exe
|
run: rm -f dist/*.exe
|
||||||
|
|
||||||
|
- name: Generate release notes
|
||||||
|
run: |
|
||||||
|
python - <<'PY'
|
||||||
|
import os
|
||||||
|
import pathlib
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
tag = os.environ["BUILD_VERSION"]
|
||||||
|
|
||||||
|
try:
|
||||||
|
previous_tag = subprocess.check_output(
|
||||||
|
["git", "describe", "--tags", "--abbrev=0", f"{tag}^"],
|
||||||
|
text=True,
|
||||||
|
).strip()
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
previous_tag = ""
|
||||||
|
|
||||||
|
log_range = f"{previous_tag}..{tag}" if previous_tag else tag
|
||||||
|
subjects = subprocess.check_output(
|
||||||
|
["git", "log", "--pretty=format:%s", log_range],
|
||||||
|
text=True,
|
||||||
|
).splitlines()
|
||||||
|
|
||||||
|
notes = []
|
||||||
|
for subject in subjects:
|
||||||
|
subject = subject.strip()
|
||||||
|
if not subject:
|
||||||
|
continue
|
||||||
|
if subject.startswith("chore: bump version"):
|
||||||
|
continue
|
||||||
|
if subject.lower() == "no message":
|
||||||
|
continue
|
||||||
|
notes.append(f"- {subject}")
|
||||||
|
|
||||||
|
if not notes:
|
||||||
|
notes = ["- Release build and version bump."]
|
||||||
|
|
||||||
|
body = "## What\'s new\n\n" + "\n".join(notes) + "\n"
|
||||||
|
pathlib.Path("release-notes.md").write_text(body, encoding="utf-8")
|
||||||
|
print(body)
|
||||||
|
PY
|
||||||
|
|
||||||
- name: Build Windows executable
|
- name: Build Windows executable
|
||||||
run: npm run build:win
|
run: npm run build:win
|
||||||
|
|
||||||
@@ -36,6 +80,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
tag_name: ${{ github.ref_name }}
|
tag_name: ${{ github.ref_name }}
|
||||||
name: gSheets-Rate-Assistant ${{ github.ref_name }}
|
name: gSheets-Rate-Assistant ${{ github.ref_name }}
|
||||||
|
body_path: release-notes.md
|
||||||
files: dist/*.exe
|
files: dist/*.exe
|
||||||
|
|
||||||
- name: Mirror release to GitHub
|
- name: Mirror release to GitHub
|
||||||
@@ -61,6 +106,8 @@ jobs:
|
|||||||
tag = os.environ["BUILD_VERSION"]
|
tag = os.environ["BUILD_VERSION"]
|
||||||
release_name = f"gSheets-Rate-Assistant {tag}"
|
release_name = f"gSheets-Rate-Assistant {tag}"
|
||||||
asset_path = next((os.path.join("dist", name) for name in os.listdir("dist") if name.endswith(".exe")), None)
|
asset_path = next((os.path.join("dist", name) for name in os.listdir("dist") if name.endswith(".exe")), None)
|
||||||
|
with open("release-notes.md", "r", encoding="utf-8") as notes_file:
|
||||||
|
release_notes = notes_file.read()
|
||||||
|
|
||||||
if asset_path is None:
|
if asset_path is None:
|
||||||
raise SystemExit("No Windows executable found in dist/")
|
raise SystemExit("No Windows executable found in dist/")
|
||||||
@@ -100,6 +147,7 @@ jobs:
|
|||||||
payload = {
|
payload = {
|
||||||
"tag_name": tag,
|
"tag_name": tag,
|
||||||
"name": release_name,
|
"name": release_name,
|
||||||
|
"body": release_notes,
|
||||||
"draft": False,
|
"draft": False,
|
||||||
"prerelease": False,
|
"prerelease": False,
|
||||||
"generate_release_notes": False,
|
"generate_release_notes": False,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ const stringifyModule = require('./node_modules/csv-stringify/dist/cjs/sync.cjs'
|
|||||||
const stringify = typeof stringifyModule === 'function'
|
const stringify = typeof stringifyModule === 'function'
|
||||||
? stringifyModule
|
? stringifyModule
|
||||||
: stringifyModule.default || stringifyModule.stringify || stringifyModule;
|
: stringifyModule.default || stringifyModule.stringify || stringifyModule;
|
||||||
|
const {version: packageVersion} = require('./package.json');
|
||||||
const {loadConfig, resolveOutputPath} = require('./lib/config');
|
const {loadConfig, resolveOutputPath} = require('./lib/config');
|
||||||
const {buildThrottle, applyRateLimitBuffer, getTotalRateLimitPerMinute} = require('./lib/rate');
|
const {buildThrottle, applyRateLimitBuffer, getTotalRateLimitPerMinute} = require('./lib/rate');
|
||||||
const {runOnboarding} = require('./lib/onboarding');
|
const {runOnboarding} = require('./lib/onboarding');
|
||||||
@@ -336,7 +337,7 @@ async function scheduleRuns(config) {
|
|||||||
throw new Error('No sheets configured to pull in config.json');
|
throw new Error('No sheets configured to pull in config.json');
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Google Sheets Rate Assistant');
|
console.log(`Google Sheets Rate Assistant v${packageVersion}`);
|
||||||
|
|
||||||
const firstApiKey = getFirstApiKey(config);
|
const firstApiKey = getFirstApiKey(config);
|
||||||
const titleMap = {};
|
const titleMap = {};
|
||||||
|
|||||||
+132
-27
@@ -1,5 +1,7 @@
|
|||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const https = require('https');
|
||||||
|
const crypto = require('crypto');
|
||||||
const {applyRateLimitBuffer} = require('./rate');
|
const {applyRateLimitBuffer} = require('./rate');
|
||||||
const {fetchSpreadsheetValuesBatch} = require('./sheets');
|
const {fetchSpreadsheetValuesBatch} = require('./sheets');
|
||||||
|
|
||||||
@@ -24,6 +26,128 @@ function resolveConfigPath(config, candidatePath) {
|
|||||||
return path.resolve(getConfigBaseDir(config), candidatePath);
|
return path.resolve(getConfigBaseDir(config), candidatePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function base64UrlEncode(value) {
|
||||||
|
return Buffer.from(value)
|
||||||
|
.toString('base64')
|
||||||
|
.replace(/=/g, '')
|
||||||
|
.replace(/\+/g, '-')
|
||||||
|
.replace(/\//g, '_');
|
||||||
|
}
|
||||||
|
|
||||||
|
function createServiceAccountAuthClient(credentials) {
|
||||||
|
const scopes = ['https://www.googleapis.com/auth/spreadsheets.readonly'];
|
||||||
|
const tokenUri = credentials.token_uri || 'https://oauth2.googleapis.com/token';
|
||||||
|
const tokenCache = {accessToken: '', expiryDate: 0};
|
||||||
|
|
||||||
|
async function requestAccessToken() {
|
||||||
|
if (!credentials.client_email || !credentials.private_key) {
|
||||||
|
throw new Error('Service account credentials must include client_email and private_key');
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
const header = {alg: 'RS256', typ: 'JWT'};
|
||||||
|
const payload = {
|
||||||
|
iss: credentials.client_email,
|
||||||
|
scope: scopes.join(' '),
|
||||||
|
aud: tokenUri,
|
||||||
|
iat: now,
|
||||||
|
exp: now + 3600,
|
||||||
|
};
|
||||||
|
const unsignedToken = `${base64UrlEncode(JSON.stringify(header))}.${base64UrlEncode(JSON.stringify(payload))}`;
|
||||||
|
const signer = crypto.createSign('RSA-SHA256');
|
||||||
|
signer.update(unsignedToken);
|
||||||
|
signer.end();
|
||||||
|
const signature = signer.sign(credentials.private_key);
|
||||||
|
const assertion = `${unsignedToken}.${base64UrlEncode(signature)}`;
|
||||||
|
const body = `grant_type=${encodeURIComponent('urn:ietf:params:oauth:grant-type:jwt-bearer')}&assertion=${encodeURIComponent(assertion)}`;
|
||||||
|
|
||||||
|
const tokenUrl = new URL(tokenUri);
|
||||||
|
const requestOptions = {
|
||||||
|
protocol: tokenUrl.protocol,
|
||||||
|
hostname: tokenUrl.hostname,
|
||||||
|
port: tokenUrl.port || 443,
|
||||||
|
method: 'POST',
|
||||||
|
path: `${tokenUrl.pathname}${tokenUrl.search}`,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'Content-Length': Buffer.byteLength(body),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const responseBody = await new Promise((resolve, reject) => {
|
||||||
|
const request = https.request(requestOptions, response => {
|
||||||
|
let responseText = '';
|
||||||
|
response.setEncoding('utf8');
|
||||||
|
|
||||||
|
response.on('data', chunk => {
|
||||||
|
responseText += chunk;
|
||||||
|
});
|
||||||
|
|
||||||
|
response.on('end', () => {
|
||||||
|
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||||
|
resolve(responseText);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let message = `Token request failed with status ${response.statusCode}`;
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(responseText);
|
||||||
|
if (parsed && parsed.error && parsed.error_description) {
|
||||||
|
message = `${parsed.error}: ${parsed.error_description}`;
|
||||||
|
} else if (parsed && parsed.error && parsed.error.message) {
|
||||||
|
message = parsed.error.message;
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
if (responseText) {
|
||||||
|
message = responseText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reject(new Error(message));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
request.on('error', reject);
|
||||||
|
request.write(body);
|
||||||
|
request.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
const tokenData = JSON.parse(responseBody);
|
||||||
|
if (!tokenData.access_token) {
|
||||||
|
throw new Error('Token response did not include access_token');
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenCache.accessToken = tokenData.access_token;
|
||||||
|
tokenCache.expiryDate = Date.now() + Math.max(0, (tokenData.expires_in || 3600) - 60) * 1000;
|
||||||
|
return tokenData.access_token;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
scopes,
|
||||||
|
async getAccessToken() {
|
||||||
|
if (tokenCache.accessToken && Date.now() < tokenCache.expiryDate) {
|
||||||
|
return {token: tokenCache.accessToken, expiry_date: tokenCache.expiryDate};
|
||||||
|
}
|
||||||
|
|
||||||
|
const accessToken = await requestAccessToken();
|
||||||
|
return {token: accessToken, expiry_date: tokenCache.expiryDate};
|
||||||
|
},
|
||||||
|
async getRequestHeaders() {
|
||||||
|
const token = await this.getAccessToken();
|
||||||
|
return {Authorization: `Bearer ${token.token}`};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function readServiceAccountCredentials(resolvedPath) {
|
||||||
|
try {
|
||||||
|
const raw = fs.readFileSync(resolvedPath, 'utf8');
|
||||||
|
return JSON.parse(raw);
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(`Failed to read/parse service account credentials at ${resolvedPath}: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function loadApiKeys(config) {
|
function loadApiKeys(config) {
|
||||||
const apiKeys = [];
|
const apiKeys = [];
|
||||||
const defaultRateLimit = config.rateLimitPerMinute || 50;
|
const defaultRateLimit = config.rateLimitPerMinute || 50;
|
||||||
@@ -85,21 +209,8 @@ function createServiceAccountBatchFetcher(config) {
|
|||||||
if (!fs.existsSync(resolvedPath)) {
|
if (!fs.existsSync(resolvedPath)) {
|
||||||
throw new Error(`Service account credentials not found: ${resolvedPath}`);
|
throw new Error(`Service account credentials not found: ${resolvedPath}`);
|
||||||
}
|
}
|
||||||
|
const credentials = readServiceAccountCredentials(resolvedPath);
|
||||||
let credentials;
|
const authClient = createServiceAccountAuthClient(credentials);
|
||||||
try {
|
|
||||||
credentials = JSON.parse(fs.readFileSync(resolvedPath, 'utf8'));
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error(`Failed to read/parse service account credentials at ${resolvedPath}: ${e.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
let authClient;
|
|
||||||
try {
|
|
||||||
authClient = google.auth.fromJSON(credentials);
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error(`Failed to initialize auth client from credentials at ${resolvedPath}: ${e.message}`);
|
|
||||||
}
|
|
||||||
authClient.scopes = ['https://www.googleapis.com/auth/spreadsheets.readonly'];
|
|
||||||
|
|
||||||
const bufferedRateLimit = applyRateLimitBuffer(rateLimitPerMinute);
|
const bufferedRateLimit = applyRateLimitBuffer(rateLimitPerMinute);
|
||||||
const interval = Math.ceil(60000 / bufferedRateLimit);
|
const interval = Math.ceil(60000 / bufferedRateLimit);
|
||||||
@@ -222,11 +333,8 @@ function getFirstServiceAccountAuthClient(config) {
|
|||||||
if (!fs.existsSync(resolvedPath)) return null;
|
if (!fs.existsSync(resolvedPath)) return null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const raw = fs.readFileSync(resolvedPath, 'utf8');
|
const credentials = readServiceAccountCredentials(resolvedPath);
|
||||||
const credentials = JSON.parse(raw);
|
return createServiceAccountAuthClient(credentials);
|
||||||
const authClient = google.auth.fromJSON(credentials);
|
|
||||||
authClient.scopes = ['https://www.googleapis.com/auth/spreadsheets.readonly'];
|
|
||||||
return authClient;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`Failed to load first service account from ${resolvedPath}: ${e.message}`);
|
console.error(`Failed to load first service account from ${resolvedPath}: ${e.message}`);
|
||||||
return null;
|
return null;
|
||||||
@@ -242,11 +350,8 @@ function loadAuth(config) {
|
|||||||
if (!fs.existsSync(resolvedPath)) {
|
if (!fs.existsSync(resolvedPath)) {
|
||||||
throw new Error(`Credentials file not found: ${resolvedPath}`);
|
throw new Error(`Credentials file not found: ${resolvedPath}`);
|
||||||
}
|
}
|
||||||
const raw = fs.readFileSync(resolvedPath, 'utf8');
|
const credentials = readServiceAccountCredentials(resolvedPath);
|
||||||
const credentials = JSON.parse(raw);
|
return createServiceAccountAuthClient(credentials);
|
||||||
const authClient = google.auth.fromJSON(credentials);
|
|
||||||
authClient.scopes = ['https://www.googleapis.com/auth/spreadsheets.readonly'];
|
|
||||||
return authClient;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return getFirstServiceAccountAuthClient(config);
|
return getFirstServiceAccountAuthClient(config);
|
||||||
@@ -258,6 +363,6 @@ module.exports = {
|
|||||||
createServiceAccountBatchFetcher,
|
createServiceAccountBatchFetcher,
|
||||||
createApiKeyBatchFetcher,
|
createApiKeyBatchFetcher,
|
||||||
getFirstApiKey,
|
getFirstApiKey,
|
||||||
getFirstServiceAccountAuthClient
|
getFirstServiceAccountAuthClient,
|
||||||
, loadAuth
|
loadAuth,
|
||||||
};
|
};
|
||||||
|
|||||||
+10
-2
@@ -1,6 +1,14 @@
|
|||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
||||||
|
let isSingleExecutableApp = false;
|
||||||
|
try {
|
||||||
|
const sea = require('node:sea');
|
||||||
|
isSingleExecutableApp = typeof sea.isSea === 'function' && sea.isSea();
|
||||||
|
} catch {
|
||||||
|
isSingleExecutableApp = false;
|
||||||
|
}
|
||||||
|
|
||||||
const CONFIG_FILE_NAME = 'config.json';
|
const CONFIG_FILE_NAME = 'config.json';
|
||||||
const DEFAULT_CONFIG_TEMPLATE = JSON.stringify({
|
const DEFAULT_CONFIG_TEMPLATE = JSON.stringify({
|
||||||
apiKeys: [
|
apiKeys: [
|
||||||
@@ -56,12 +64,12 @@ const DEFAULT_CONFIG_TEMPLATE = JSON.stringify({
|
|||||||
}, null, 2) + '\n';
|
}, null, 2) + '\n';
|
||||||
|
|
||||||
function getDefaultConfigPath() {
|
function getDefaultConfigPath() {
|
||||||
const packagedApp = Boolean(process.pkg);
|
const packagedApp = isSingleExecutableApp;
|
||||||
return path.resolve(packagedApp ? path.dirname(process.execPath) : process.cwd(), CONFIG_FILE_NAME);
|
return path.resolve(packagedApp ? path.dirname(process.execPath) : process.cwd(), CONFIG_FILE_NAME);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getConfigPathCandidates() {
|
function getConfigPathCandidates() {
|
||||||
const packagedApp = Boolean(process.pkg);
|
const packagedApp = isSingleExecutableApp;
|
||||||
const externalDefault = getDefaultConfigPath();
|
const externalDefault = getDefaultConfigPath();
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|||||||
+13
-11
@@ -45,13 +45,16 @@ function extractErrorMessage(data) {
|
|||||||
function requestJson(urlString, headers = {}) {
|
function requestJson(urlString, headers = {}) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const url = new URL(urlString);
|
const url = new URL(urlString);
|
||||||
const request = https.request(
|
const requestOptions = {
|
||||||
url,
|
protocol: url.protocol,
|
||||||
{
|
hostname: url.hostname,
|
||||||
|
port: url.port || 443,
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
|
path: `${url.pathname}${url.search}`,
|
||||||
headers: Object.assign({Accept: 'application/json'}, headers),
|
headers: Object.assign({Accept: 'application/json'}, headers),
|
||||||
},
|
};
|
||||||
response => {
|
|
||||||
|
const request = https.request(requestOptions, response => {
|
||||||
let body = '';
|
let body = '';
|
||||||
response.setEncoding('utf8');
|
response.setEncoding('utf8');
|
||||||
|
|
||||||
@@ -72,8 +75,7 @@ function requestJson(urlString, headers = {}) {
|
|||||||
error.response = {status: response.statusCode, headers: response.headers, data};
|
error.response = {status: response.statusCode, headers: response.headers, data};
|
||||||
reject(error);
|
reject(error);
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
request.on('error', reject);
|
request.on('error', reject);
|
||||||
request.end();
|
request.end();
|
||||||
@@ -96,15 +98,15 @@ async function requestSheetsJson(pathname, context, queryParams = {}) {
|
|||||||
const headers = {};
|
const headers = {};
|
||||||
|
|
||||||
if (context && context.authClient) {
|
if (context && context.authClient) {
|
||||||
if (typeof context.authClient.getRequestHeaders === 'function') {
|
if (typeof context.authClient.getAccessToken === 'function') {
|
||||||
const authHeaders = await context.authClient.getRequestHeaders(url.toString());
|
|
||||||
Object.assign(headers, authHeaders || {});
|
|
||||||
} else if (typeof context.authClient.getAccessToken === 'function') {
|
|
||||||
const token = await context.authClient.getAccessToken();
|
const token = await context.authClient.getAccessToken();
|
||||||
const accessToken = token && typeof token === 'object' ? token.token || token.access_token : token;
|
const accessToken = token && typeof token === 'object' ? token.token || token.access_token : token;
|
||||||
if (accessToken) {
|
if (accessToken) {
|
||||||
headers.Authorization = `Bearer ${accessToken}`;
|
headers.Authorization = `Bearer ${accessToken}`;
|
||||||
}
|
}
|
||||||
|
} else if (typeof context.authClient.getRequestHeaders === 'function') {
|
||||||
|
const authHeaders = await context.authClient.getRequestHeaders(url.toString());
|
||||||
|
Object.assign(headers, authHeaders || {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Generated
+459
-1522
File diff suppressed because it is too large
Load Diff
+2
-7
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "google-sheets-rate-assistant",
|
"name": "google-sheets-rate-assistant",
|
||||||
"version": "2.1.9",
|
"version": "2.1.11",
|
||||||
"description": "Pull Google Sheets data and export to CSV with configurable schedules and rate limiting.",
|
"description": "Pull Google Sheets data and export to CSV with configurable schedules and rate limiting.",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"bin": "index.js",
|
"bin": "index.js",
|
||||||
@@ -14,11 +14,6 @@
|
|||||||
"p-throttle": "^8.1.0"
|
"p-throttle": "^8.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"pkg": "^5.8.1"
|
"esbuild": "^0.25.9"
|
||||||
},
|
|
||||||
"pkg": {
|
|
||||||
"assets": [
|
|
||||||
"node_modules/csv-stringify/dist/cjs/sync.cjs"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ This Node.js application pulls configured Google Sheets documents and exports th
|
|||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
Download the latest version from the [Releases tab](./releases) and run it.
|
Download the latest version from the Releases and run it.
|
||||||
|
|
||||||
1. If `config.json` does not exist, the app will ask whether you want a wizard or a starter config file.
|
1. If `config.json` does not exist, the app will ask whether you want a wizard or a starter config file.
|
||||||
2. If you choose the config-file path instead of the wizard, configure authentication in `config.json` — choose one of:
|
2. If you choose the config-file path instead of the wizard, configure authentication in `config.json` — choose one of:
|
||||||
@@ -63,6 +63,10 @@ If `config.json` is missing, the app creates a starter file next to the executab
|
|||||||
|
|
||||||
If you choose the wizard, it will ask for the auth method, credential count, document count, and sheet names. It will not ask for output directories or file names.
|
If you choose the wizard, it will ask for the auth method, credential count, document count, and sheet names. It will not ask for output directories or file names.
|
||||||
|
|
||||||
|
## Build Note
|
||||||
|
|
||||||
|
The Windows packaging script in `scripts/build-win.js` now builds a single executable using Node 26's SEA workflow.
|
||||||
|
|
||||||
## AI Disclosure
|
## AI Disclosure
|
||||||
|
|
||||||
A large chunk of the project was coded using AI, this is the first pass at integrating some AI assistance into my projects. However all code written has been verified and checked before submission. AI use has only been included since the v2 rewrite.
|
A large chunk of the project was coded using AI, this is the first pass at integrating some AI assistance into my projects. However all code written has been verified and checked before submission. AI use has only been included since the v2 rewrite.
|
||||||
|
|||||||
+28
-2
@@ -1,6 +1,8 @@
|
|||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
|
const os = require('os');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const {exec} = require('pkg');
|
const {execFileSync} = require('child_process');
|
||||||
|
const esbuild = require('esbuild');
|
||||||
const {version: packageVersion} = require('../package.json');
|
const {version: packageVersion} = require('../package.json');
|
||||||
|
|
||||||
function normalizeBuildVersion(value) {
|
function normalizeBuildVersion(value) {
|
||||||
@@ -14,9 +16,33 @@ async function main() {
|
|||||||
fs.mkdirSync(distDir, {recursive: true});
|
fs.mkdirSync(distDir, {recursive: true});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsa-sea-'));
|
||||||
|
const bundlePath = path.join(tempDir, 'index.cjs');
|
||||||
|
const seaBlobPath = path.join(tempDir, 'sea-prep.blob');
|
||||||
|
const seaConfigPath = path.join(tempDir, 'sea-config.json');
|
||||||
const buildVersion = normalizeBuildVersion(process.env.BUILD_VERSION);
|
const buildVersion = normalizeBuildVersion(process.env.BUILD_VERSION);
|
||||||
const outputFile = path.join(distDir, `gSheets-Rate-Assistant-${buildVersion}.exe`);
|
const outputFile = path.join(distDir, `gSheets-Rate-Assistant-${buildVersion}.exe`);
|
||||||
await exec(['.', '--targets', 'latest-win-x64', '--output', outputFile]);
|
|
||||||
|
await esbuild.build({
|
||||||
|
entryPoints: [path.resolve(__dirname, '..', 'index.js')],
|
||||||
|
bundle: true,
|
||||||
|
platform: 'node',
|
||||||
|
format: 'cjs',
|
||||||
|
target: 'node26',
|
||||||
|
outfile: bundlePath,
|
||||||
|
logLevel: 'info',
|
||||||
|
});
|
||||||
|
|
||||||
|
fs.writeFileSync(seaConfigPath, JSON.stringify({
|
||||||
|
main: bundlePath,
|
||||||
|
output: outputFile,
|
||||||
|
mainFormat: 'commonjs',
|
||||||
|
disableExperimentalSEAWarning: true,
|
||||||
|
useCodeCache: false,
|
||||||
|
useSnapshot: false,
|
||||||
|
}, null, 2));
|
||||||
|
|
||||||
|
execFileSync(process.execPath, ['--build-sea', seaConfigPath], {stdio: 'inherit'});
|
||||||
}
|
}
|
||||||
|
|
||||||
main().catch(error => {
|
main().catch(error => {
|
||||||
|
|||||||
Reference in New Issue
Block a user