@ghl-ai/aw 0.1.41-beta.0 → 0.1.41-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli.mjs +1 -0
- package/commands/init.mjs +55 -15
- package/commands/push.mjs +6 -11
- package/git.mjs +101 -19
- package/package.json +1 -1
- package/telemetry.mjs +13 -3
package/cli.mjs
CHANGED
|
@@ -26,6 +26,7 @@ const COMMANDS = {
|
|
|
26
26
|
nuke: () => import('./commands/nuke.mjs').then(m => m.nukeCommand),
|
|
27
27
|
daemon: () => import('./commands/daemon.mjs').then(m => m.daemonCommand),
|
|
28
28
|
telemetry: () => import('./commands/telemetry.mjs').then(m => m.telemetryCommand),
|
|
29
|
+
'slack-sim': () => import('./commands/slack-sim.mjs').then(m => m.slackSimCommand),
|
|
29
30
|
};
|
|
30
31
|
|
|
31
32
|
function parseArgs(argv) {
|
package/commands/init.mjs
CHANGED
|
@@ -13,7 +13,6 @@ import {
|
|
|
13
13
|
readFileSync,
|
|
14
14
|
rmSync,
|
|
15
15
|
realpathSync,
|
|
16
|
-
appendFileSync,
|
|
17
16
|
} from 'node:fs';
|
|
18
17
|
import { execSync } from 'node:child_process';
|
|
19
18
|
import { join, dirname, sep } from 'node:path';
|
|
@@ -88,24 +87,65 @@ function syncHomeAndProjectInstructions(cwd, namespace) {
|
|
|
88
87
|
}
|
|
89
88
|
}
|
|
90
89
|
|
|
91
|
-
// ── Ensure ~/.aw/.
|
|
92
|
-
|
|
93
|
-
|
|
90
|
+
// ── Ensure ~/.aw/.git/info/exclude has the whitelist block ─────────────
|
|
91
|
+
//
|
|
92
|
+
// Strategy: only .aw_registry/, .aw_rules/, content/ are tracked — everything
|
|
93
|
+
// else at the top level of ~/.aw/ is local-only (telemetry/, hooks/, logs,
|
|
94
|
+
// .DS_Store, etc.). We write to .git/info/exclude (not tracked .gitignore)
|
|
95
|
+
// so upstream pulls never conflict.
|
|
96
|
+
|
|
97
|
+
const AW_MANAGED_BEGIN = '# BEGIN aw-managed (do not edit; managed by `aw init`)';
|
|
98
|
+
const AW_MANAGED_END = '# END aw-managed';
|
|
99
|
+
|
|
100
|
+
const AW_MANAGED_BLOCK = [
|
|
101
|
+
AW_MANAGED_BEGIN,
|
|
102
|
+
'# Whitelist: only these top-level entries are tracked; everything else is local-only.',
|
|
103
|
+
'/*',
|
|
104
|
+
'!/.aw_registry',
|
|
105
|
+
'!/.aw_rules',
|
|
106
|
+
'!/content',
|
|
107
|
+
'',
|
|
108
|
+
'# Nested local state within whitelisted dirs',
|
|
109
|
+
'/.aw_registry/.sync-config.json',
|
|
110
|
+
AW_MANAGED_END,
|
|
111
|
+
'',
|
|
112
|
+
].join('\n');
|
|
113
|
+
|
|
114
|
+
// Legacy flat lines appended by earlier versions of ensureAwGitignore — strip on upgrade
|
|
115
|
+
// so we don't leave stale rules lingering outside the managed block.
|
|
116
|
+
const LEGACY_GITIGNORE_LINES = new Set([
|
|
94
117
|
'.aw_registry/.sync-config.json',
|
|
95
118
|
'hooks/',
|
|
96
|
-
|
|
119
|
+
'# aw: personal/local — do not commit',
|
|
120
|
+
]);
|
|
121
|
+
|
|
122
|
+
function escapeRegex(s) {
|
|
123
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
124
|
+
}
|
|
97
125
|
|
|
98
|
-
function ensureAwGitignore(awHome) {
|
|
99
|
-
// Use .git/info/exclude so the tracked .gitignore stays clean
|
|
126
|
+
export function ensureAwGitignore(awHome) {
|
|
100
127
|
const excludePath = join(awHome, '.git', 'info', 'exclude');
|
|
101
128
|
let existing = '';
|
|
102
|
-
try { existing = readFileSync(excludePath, 'utf8'); } catch { /* doesn't exist yet */ }
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
129
|
+
try { existing = readFileSync(excludePath, 'utf8'); } catch (err) { void err; /* doesn't exist yet */ }
|
|
130
|
+
|
|
131
|
+
// Strip any prior aw-managed block so re-rendering is idempotent.
|
|
132
|
+
const blockRegex = new RegExp(
|
|
133
|
+
`${escapeRegex(AW_MANAGED_BEGIN)}[\\s\\S]*?${escapeRegex(AW_MANAGED_END)}\\n?`,
|
|
134
|
+
'g'
|
|
135
|
+
);
|
|
136
|
+
const withoutManaged = existing.replace(blockRegex, '');
|
|
137
|
+
|
|
138
|
+
// Strip legacy flat lines (pre-whitelist implementation).
|
|
139
|
+
const cleaned = withoutManaged
|
|
140
|
+
.split('\n')
|
|
141
|
+
.filter(line => !LEGACY_GITIGNORE_LINES.has(line.trim()))
|
|
142
|
+
.join('\n');
|
|
143
|
+
|
|
144
|
+
const prefix = cleaned === '' || cleaned.endsWith('\n') ? cleaned : cleaned + '\n';
|
|
145
|
+
const next = prefix + AW_MANAGED_BLOCK;
|
|
146
|
+
|
|
147
|
+
if (next === existing) return; // already up to date
|
|
148
|
+
try { writeFileSync(excludePath, next); } catch (err) { void err; /* best effort */ }
|
|
109
149
|
}
|
|
110
150
|
|
|
111
151
|
// ── IDE tasks for auto-pull ─────────────────────────────────────────────
|
|
@@ -371,7 +411,7 @@ export async function initCommand(args) {
|
|
|
371
411
|
}
|
|
372
412
|
|
|
373
413
|
// Determine sparse paths
|
|
374
|
-
const sparsePaths = [`.aw_registry/platform`, `content`, RULES_SOURCE_DIR, `.aw_registry/AW-PROTOCOL.md`, `CODEOWNERS
|
|
414
|
+
const sparsePaths = [`.aw_registry/platform`, `content`, RULES_SOURCE_DIR, `.aw_registry/AW-PROTOCOL.md`, `CODEOWNERS`];
|
|
375
415
|
if (folderName) {
|
|
376
416
|
sparsePaths.push(`.aw_registry/${folderName}`);
|
|
377
417
|
}
|
package/commands/push.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// commands/push.mjs — Push local agents/skills to registry via PR using persistent git clone
|
|
2
2
|
|
|
3
3
|
import { existsSync, statSync, readFileSync, appendFileSync } from 'node:fs';
|
|
4
|
-
import { join, dirname
|
|
4
|
+
import { join, dirname } from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { exec as execCb, execFile as execFileCb } from 'node:child_process';
|
|
7
7
|
import { promisify } from 'node:util';
|
|
@@ -361,11 +361,7 @@ async function doPush(files, awHome, dryRun, worktreeFlow = false, preStaged = f
|
|
|
361
361
|
|
|
362
362
|
// CODEOWNERS for new namespaces (runs inside spinner so no silent gap)
|
|
363
363
|
const topNamespaces = [...new Set(files.map(f => f.namespace.split('/')[0]))];
|
|
364
|
-
|
|
365
|
-
// platform-docs has .github/CODEOWNERS as the authoritative file.
|
|
366
|
-
const codeownersPath = existsSync(join(awHome, '.github', 'CODEOWNERS'))
|
|
367
|
-
? join(awHome, '.github', 'CODEOWNERS')
|
|
368
|
-
: join(awHome, 'CODEOWNERS');
|
|
364
|
+
const codeownersPath = join(awHome, 'CODEOWNERS');
|
|
369
365
|
const newNamespaces = [];
|
|
370
366
|
const ghUser = await getGitHubUser();
|
|
371
367
|
for (const ns of topNamespaces) {
|
|
@@ -378,8 +374,7 @@ async function doPush(files, awHome, dryRun, worktreeFlow = false, preStaged = f
|
|
|
378
374
|
|
|
379
375
|
const pathsToStage = files.map(f => f.registryTarget);
|
|
380
376
|
if (newNamespaces.length > 0 && existsSync(codeownersPath)) {
|
|
381
|
-
|
|
382
|
-
pathsToStage.push(relative(awHome, codeownersPath));
|
|
377
|
+
pathsToStage.push('CODEOWNERS');
|
|
383
378
|
}
|
|
384
379
|
// Also stage any extra paths (content/, CODEOWNERS manual edits) passed from the caller
|
|
385
380
|
for (const p of extraPaths) {
|
|
@@ -478,17 +473,17 @@ export async function pushCommand(args) {
|
|
|
478
473
|
if (!input) {
|
|
479
474
|
const rulesChanged = hasRulesChanges(cwd);
|
|
480
475
|
|
|
481
|
-
// Extra paths outside .aw_registry/ that aw also manages: content
|
|
476
|
+
// Extra paths outside .aw_registry/ that aw also manages: content/ and CODEOWNERS.
|
|
482
477
|
// Detect staged variants for staged-mode and unstaged variants for auto-mode.
|
|
483
478
|
const getExtraStagedPaths = async () => {
|
|
484
479
|
try {
|
|
485
|
-
const { stdout } = await exec(`git -C "${awHome}" diff --cached --name-only -- content/ CODEOWNERS
|
|
480
|
+
const { stdout } = await exec(`git -C "${awHome}" diff --cached --name-only -- content/ CODEOWNERS`);
|
|
486
481
|
return stdout.trim().split('\n').filter(Boolean);
|
|
487
482
|
} catch { return []; }
|
|
488
483
|
};
|
|
489
484
|
const getExtraChangedPaths = async () => {
|
|
490
485
|
try {
|
|
491
|
-
const { stdout } = await exec(`git -C "${awHome}" status --porcelain -- content/ CODEOWNERS
|
|
486
|
+
const { stdout } = await exec(`git -C "${awHome}" status --porcelain -- content/ CODEOWNERS`);
|
|
492
487
|
// git status --porcelain prefix is XY (2 chars) + optional space + path.
|
|
493
488
|
// Staged-only files: `M path` (2-char prefix); unstaged files: ` M path` (3-char prefix).
|
|
494
489
|
// slice(2).trimStart() handles both cases correctly.
|
package/git.mjs
CHANGED
|
@@ -9,6 +9,49 @@ import { REGISTRY_BASE_BRANCH, REGISTRY_DIR, DOCS_SOURCE_DIR, RULES_SOURCE_DIR }
|
|
|
9
9
|
|
|
10
10
|
const exec = promisify(execCb);
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Env vars applied to every git command that touches the network.
|
|
14
|
+
* GIT_TERMINAL_PROMPT=0 prevents git from hanging when it would otherwise
|
|
15
|
+
* prompt for credentials (e.g. HTTPS URL with SSH-only auth configured).
|
|
16
|
+
* Instead, git exits immediately with a non-zero code that we can catch.
|
|
17
|
+
*/
|
|
18
|
+
const GIT_NET_ENV = { ...process.env, GIT_TERMINAL_PROMPT: '0' };
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Convert an HTTPS GitHub URL to its SSH equivalent.
|
|
22
|
+
* e.g. https://github.com/Org/Repo.git → git@github.com:Org/Repo.git
|
|
23
|
+
*/
|
|
24
|
+
export function toSshUrl(httpsUrl) {
|
|
25
|
+
const m = httpsUrl.match(/^https:\/\/github\.com\/(.+)$/);
|
|
26
|
+
return m ? `git@github.com:${m[1]}` : httpsUrl;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Detect whether the user's git is configured to prefer SSH for github.com.
|
|
31
|
+
* Checks: 1) git insteadOf config 2) gh CLI auth status
|
|
32
|
+
* Returns true if SSH is preferred.
|
|
33
|
+
*/
|
|
34
|
+
export function prefersSsh() {
|
|
35
|
+
// Check git url."git@github.com:".insteadOf
|
|
36
|
+
try {
|
|
37
|
+
const out = execSync(
|
|
38
|
+
'git config --global --get-regexp "url\\.git@github\\.com.*\\.insteadOf"',
|
|
39
|
+
{ stdio: 'pipe', encoding: 'utf8', env: GIT_NET_ENV },
|
|
40
|
+
).trim();
|
|
41
|
+
if (out.includes('https://github.com')) return true;
|
|
42
|
+
} catch { /* not configured */ }
|
|
43
|
+
|
|
44
|
+
// Check gh auth — if protocol is ssh, prefer SSH
|
|
45
|
+
try {
|
|
46
|
+
const out = execSync('gh auth status 2>&1', {
|
|
47
|
+
stdio: 'pipe', encoding: 'utf8', env: GIT_NET_ENV, timeout: 5000,
|
|
48
|
+
});
|
|
49
|
+
if (/git protocol:\s*ssh/i.test(out)) return true;
|
|
50
|
+
} catch { /* gh not installed or not authed */ }
|
|
51
|
+
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
12
55
|
// ── Backward-compat: temp-dir sparse checkout (used by search.mjs) ────────────
|
|
13
56
|
|
|
14
57
|
/**
|
|
@@ -18,12 +61,23 @@ const exec = promisify(execCb);
|
|
|
18
61
|
export function sparseCheckout(repo, paths) {
|
|
19
62
|
const tempDir = mkdtempSync(join(tmpdir(), 'aw-'));
|
|
20
63
|
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
64
|
+
const httpsUrl = repo.startsWith('http') ? repo : `https://github.com/${repo}.git`;
|
|
65
|
+
const urls = prefersSsh() ? [toSshUrl(httpsUrl), httpsUrl] : [httpsUrl, toSshUrl(httpsUrl)];
|
|
66
|
+
|
|
67
|
+
let cloned = false;
|
|
68
|
+
for (const url of urls) {
|
|
69
|
+
try {
|
|
70
|
+
execSync(`git clone --filter=blob:none --no-checkout "${url}" "${tempDir}"`, {
|
|
71
|
+
stdio: 'pipe', env: GIT_NET_ENV,
|
|
72
|
+
});
|
|
73
|
+
cloned = true;
|
|
74
|
+
break;
|
|
75
|
+
} catch {
|
|
76
|
+
// Clean up partial clone so next attempt can use the same tempDir
|
|
77
|
+
try { rmSync(join(tempDir, '.git'), { recursive: true, force: true }); } catch {}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (!cloned) {
|
|
27
81
|
throw new Error(`Failed to clone ${repo}. Check your git credentials and repo access.`);
|
|
28
82
|
}
|
|
29
83
|
|
|
@@ -47,10 +101,20 @@ export function sparseCheckout(repo, paths) {
|
|
|
47
101
|
export async function sparseCheckoutAsync(repo, paths) {
|
|
48
102
|
const tempDir = mkdtempSync(join(tmpdir(), 'aw-'));
|
|
49
103
|
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
104
|
+
const httpsUrl = repo.startsWith('http') ? repo : `https://github.com/${repo}.git`;
|
|
105
|
+
const urls = prefersSsh() ? [toSshUrl(httpsUrl), httpsUrl] : [httpsUrl, toSshUrl(httpsUrl)];
|
|
106
|
+
|
|
107
|
+
let cloned = false;
|
|
108
|
+
for (const url of urls) {
|
|
109
|
+
try {
|
|
110
|
+
await exec(`git clone --filter=blob:none --no-checkout "${url}" "${tempDir}"`, { env: GIT_NET_ENV });
|
|
111
|
+
cloned = true;
|
|
112
|
+
break;
|
|
113
|
+
} catch {
|
|
114
|
+
try { rmSync(join(tempDir, '.git'), { recursive: true, force: true }); } catch {}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (!cloned) {
|
|
54
118
|
throw new Error(`Failed to clone ${repo}. Check your git credentials and repo access.`);
|
|
55
119
|
}
|
|
56
120
|
|
|
@@ -106,7 +170,9 @@ export function isValidClone(awHome, repoUrl) {
|
|
|
106
170
|
if (!existsSync(join(awHome, '.git'))) return false;
|
|
107
171
|
try {
|
|
108
172
|
const remote = execSync('git remote get-url origin', { cwd: awHome, stdio: 'pipe', encoding: 'utf8' }).trim();
|
|
109
|
-
|
|
173
|
+
// Normalize both sides to bare repo path for comparison (handles HTTPS ↔ SSH)
|
|
174
|
+
const normalize = (u) => u.replace(/\.git$/, '').replace(/^git@github\.com:/, 'https://github.com/');
|
|
175
|
+
return normalize(remote) === normalize(repoUrl);
|
|
110
176
|
} catch {
|
|
111
177
|
return false;
|
|
112
178
|
}
|
|
@@ -123,10 +189,26 @@ export async function initPersistentClone(repoUrl, awHome, sparsePaths) {
|
|
|
123
189
|
try { execSync(`rm -rf "${awHome}"`, { stdio: 'pipe' }); } catch {}
|
|
124
190
|
}
|
|
125
191
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
192
|
+
const urls = prefersSsh()
|
|
193
|
+
? [toSshUrl(repoUrl), repoUrl]
|
|
194
|
+
: [repoUrl, toSshUrl(repoUrl)];
|
|
195
|
+
|
|
196
|
+
let cloned = false;
|
|
197
|
+
for (const url of urls) {
|
|
198
|
+
// Clean up any partial clone from a previous attempt
|
|
199
|
+
if (existsSync(awHome) && !isValidClone(awHome, url)) {
|
|
200
|
+
try { execSync(`rm -rf "${awHome}"`, { stdio: 'pipe' }); } catch {}
|
|
201
|
+
}
|
|
202
|
+
try {
|
|
203
|
+
await exec(`git clone --filter=blob:none --no-checkout "${url}" "${awHome}"`, { env: GIT_NET_ENV });
|
|
204
|
+
cloned = true;
|
|
205
|
+
break;
|
|
206
|
+
} catch {
|
|
207
|
+
// try next URL
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (!cloned) {
|
|
211
|
+
throw new Error(`Failed to clone ${repoUrl}. Check your git credentials and repo access (HTTPS and SSH both failed).`);
|
|
130
212
|
}
|
|
131
213
|
|
|
132
214
|
try {
|
|
@@ -286,7 +368,7 @@ export async function fetchAndMerge(awHome, { silent = true } = {}) {
|
|
|
286
368
|
|
|
287
369
|
// ── 2. Fetch ──────────────────────────────────────────────────────────────
|
|
288
370
|
try {
|
|
289
|
-
await exec(`git -C "${awHome}" fetch origin ${REGISTRY_BASE_BRANCH}
|
|
371
|
+
await exec(`git -C "${awHome}" fetch origin ${REGISTRY_BASE_BRANCH}`, { env: GIT_NET_ENV });
|
|
290
372
|
} catch (e) {
|
|
291
373
|
throw new Error(`Failed to fetch from origin: ${e.message}`);
|
|
292
374
|
}
|
|
@@ -315,7 +397,7 @@ export async function fetchAndMerge(awHome, { silent = true } = {}) {
|
|
|
315
397
|
// someone else pushed to the remote tracking branch since our last fetch.
|
|
316
398
|
if (isPushBranch) {
|
|
317
399
|
try {
|
|
318
|
-
await exec(`git -C "${awHome}" push --force-with-lease origin "${currentBranch}"
|
|
400
|
+
await exec(`git -C "${awHome}" push --force-with-lease origin "${currentBranch}"`, { env: GIT_NET_ENV });
|
|
319
401
|
} catch { /* non-blocking — divergence will be resolved on next aw push */ }
|
|
320
402
|
}
|
|
321
403
|
} catch {
|
|
@@ -442,7 +524,7 @@ export function updatePushBranch(awHome, pushBranchName) {
|
|
|
442
524
|
}
|
|
443
525
|
|
|
444
526
|
try {
|
|
445
|
-
execSync(`git -C "${awHome}" push origin "${pushBranchName}" --force`, { stdio: 'pipe' });
|
|
527
|
+
execSync(`git -C "${awHome}" push origin "${pushBranchName}" --force`, { stdio: 'pipe', env: GIT_NET_ENV });
|
|
446
528
|
} catch (e) {
|
|
447
529
|
throw new Error(`Failed to push branch: ${e.message}`);
|
|
448
530
|
}
|
|
@@ -485,7 +567,7 @@ export async function createPushBranch(awHome, branchName, files, commitMsg, pre
|
|
|
485
567
|
}
|
|
486
568
|
|
|
487
569
|
try {
|
|
488
|
-
await exec(`git -C "${awHome}" push -u origin "${branchName}"
|
|
570
|
+
await exec(`git -C "${awHome}" push -u origin "${branchName}"`, { env: GIT_NET_ENV });
|
|
489
571
|
} catch (e) {
|
|
490
572
|
throw new Error(`Failed to push branch: ${e.message}`);
|
|
491
573
|
}
|
package/package.json
CHANGED
package/telemetry.mjs
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { createHash, randomUUID } from 'node:crypto';
|
|
9
9
|
import { hostname, userInfo, platform, arch, release } from 'node:os';
|
|
10
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync } from 'node:fs';
|
|
10
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, lstatSync, unlinkSync } from 'node:fs';
|
|
11
11
|
import { join, dirname } from 'node:path';
|
|
12
12
|
import { fileURLToPath } from 'node:url';
|
|
13
13
|
import { execSync } from 'node:child_process';
|
|
@@ -16,7 +16,9 @@ import { TELEMETRY_URL, AW_HOME } from './constants.mjs';
|
|
|
16
16
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
17
17
|
const VERSION = JSON.parse(readFileSync(join(__dirname, 'package.json'), 'utf8')).version;
|
|
18
18
|
|
|
19
|
-
const
|
|
19
|
+
const CONFIG_DIR = join(AW_HOME, 'telemetry');
|
|
20
|
+
const CONFIG_PATH = join(CONFIG_DIR, 'config.json');
|
|
21
|
+
const LEGACY_CONFIG_PATH = join(AW_HOME, '.telemetry');
|
|
20
22
|
|
|
21
23
|
// ── Config ──────────────────────────────────────────────────────────
|
|
22
24
|
|
|
@@ -30,7 +32,15 @@ export function loadConfig() {
|
|
|
30
32
|
if (existsSync(CONFIG_PATH)) {
|
|
31
33
|
return JSON.parse(readFileSync(CONFIG_PATH, 'utf8'));
|
|
32
34
|
}
|
|
33
|
-
|
|
35
|
+
// One-time migration from legacy single-file shape (~/.aw/.telemetry).
|
|
36
|
+
// Only triggered when new path is missing, so re-running is a no-op.
|
|
37
|
+
if (existsSync(LEGACY_CONFIG_PATH) && lstatSync(LEGACY_CONFIG_PATH).isFile()) {
|
|
38
|
+
const legacy = JSON.parse(readFileSync(LEGACY_CONFIG_PATH, 'utf8'));
|
|
39
|
+
saveConfig(legacy);
|
|
40
|
+
try { unlinkSync(LEGACY_CONFIG_PATH); } catch (err) { void err; /* best-effort cleanup */ }
|
|
41
|
+
return legacy;
|
|
42
|
+
}
|
|
43
|
+
} catch (err) { void err; /* corrupt file — fall through to fresh config */ }
|
|
34
44
|
|
|
35
45
|
const config = {
|
|
36
46
|
machine_id: generateMachineId(),
|