@aligndottech/cli 0.57.0 → 0.58.0
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/README.md +31 -18
- package/dist/bin.d.ts +3 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +14 -0
- package/dist/bin.js.map +1 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +24 -2
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/import/git.d.ts.map +1 -1
- package/dist/commands/import/git.js +8 -4
- package/dist/commands/import/git.js.map +1 -1
- package/dist/commands/mcp.js +1 -1
- package/dist/commands/mcp.js.map +1 -1
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +26 -12
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/use.d.ts +2 -0
- package/dist/commands/use.d.ts.map +1 -1
- package/dist/commands/use.js +19 -1
- package/dist/commands/use.js.map +1 -1
- package/dist/commands/why.d.ts.map +1 -1
- package/dist/commands/why.js +91 -53
- package/dist/commands/why.js.map +1 -1
- package/dist/lib/agent-rules.d.ts +11 -4
- package/dist/lib/agent-rules.d.ts.map +1 -1
- package/dist/lib/agent-rules.js +163 -190
- package/dist/lib/agent-rules.js.map +1 -1
- package/dist/lib/ask-listing.d.ts +30 -0
- package/dist/lib/ask-listing.d.ts.map +1 -0
- package/dist/lib/ask-listing.js +60 -0
- package/dist/lib/ask-listing.js.map +1 -0
- package/dist/lib/capture-report.d.ts.map +1 -1
- package/dist/lib/capture-report.js +11 -3
- package/dist/lib/capture-report.js.map +1 -1
- package/dist/lib/claude-presence.d.ts +12 -0
- package/dist/lib/claude-presence.d.ts.map +1 -0
- package/dist/lib/claude-presence.js +19 -0
- package/dist/lib/claude-presence.js.map +1 -0
- package/dist/lib/config.d.ts +2 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +4 -0
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/doctor.d.ts +9 -1
- package/dist/lib/doctor.d.ts.map +1 -1
- package/dist/lib/doctor.js +19 -1
- package/dist/lib/doctor.js.map +1 -1
- package/dist/lib/embedding-assets.d.ts +40 -0
- package/dist/lib/embedding-assets.d.ts.map +1 -0
- package/dist/lib/embedding-assets.js +74 -0
- package/dist/lib/embedding-assets.js.map +1 -0
- package/dist/lib/embedding-runtime.d.ts +41 -0
- package/dist/lib/embedding-runtime.d.ts.map +1 -0
- package/dist/lib/embedding-runtime.js +224 -0
- package/dist/lib/embedding-runtime.js.map +1 -0
- package/dist/lib/fetchers/git.d.ts +3 -0
- package/dist/lib/fetchers/git.d.ts.map +1 -1
- package/dist/lib/fetchers/git.js +9 -5
- package/dist/lib/fetchers/git.js.map +1 -1
- package/dist/lib/found-summary.d.ts.map +1 -1
- package/dist/lib/found-summary.js +19 -3
- package/dist/lib/found-summary.js.map +1 -1
- package/dist/lib/gateway-client.d.ts.map +1 -1
- package/dist/lib/gateway-client.js +21 -1
- package/dist/lib/gateway-client.js.map +1 -1
- package/dist/lib/git.d.ts +7 -0
- package/dist/lib/git.d.ts.map +1 -1
- package/dist/lib/git.js +7 -2
- package/dist/lib/git.js.map +1 -1
- package/dist/lib/import-defaults.d.ts +11 -4
- package/dist/lib/import-defaults.d.ts.map +1 -1
- package/dist/lib/import-defaults.js +11 -4
- package/dist/lib/import-defaults.js.map +1 -1
- package/dist/lib/launch/mcp-child-env.d.ts.map +1 -1
- package/dist/lib/launch/mcp-child-env.js +6 -4
- package/dist/lib/launch/mcp-child-env.js.map +1 -1
- package/dist/lib/local-db.d.ts +13 -0
- package/dist/lib/local-db.d.ts.map +1 -1
- package/dist/lib/local-db.js +23 -0
- package/dist/lib/local-db.js.map +1 -1
- package/dist/lib/local-embeddings-native.d.ts +17 -0
- package/dist/lib/local-embeddings-native.d.ts.map +1 -0
- package/dist/lib/local-embeddings-native.js +78 -0
- package/dist/lib/local-embeddings-native.js.map +1 -0
- package/dist/lib/local-embeddings.d.ts.map +1 -1
- package/dist/lib/local-embeddings.js +46 -47
- package/dist/lib/local-embeddings.js.map +1 -1
- package/dist/lib/local-gateway-client.d.ts +2 -2
- package/dist/lib/local-gateway-client.d.ts.map +1 -1
- package/dist/lib/local-gateway-client.js +43 -31
- package/dist/lib/local-gateway-client.js.map +1 -1
- package/dist/lib/local-thresholds.d.ts +8 -0
- package/dist/lib/local-thresholds.d.ts.map +1 -1
- package/dist/lib/local-thresholds.js +8 -0
- package/dist/lib/local-thresholds.js.map +1 -1
- package/dist/lib/model-cache.d.ts +13 -0
- package/dist/lib/model-cache.d.ts.map +1 -1
- package/dist/lib/model-cache.js +26 -0
- package/dist/lib/model-cache.js.map +1 -1
- package/dist/lib/next-step.d.ts +2 -0
- package/dist/lib/next-step.d.ts.map +1 -1
- package/dist/lib/next-step.js +37 -16
- package/dist/lib/next-step.js.map +1 -1
- package/dist/lib/node-floor.d.ts +6 -0
- package/dist/lib/node-floor.d.ts.map +1 -0
- package/dist/lib/node-floor.js +15 -0
- package/dist/lib/node-floor.js.map +1 -0
- package/dist/lib/safe-config-write.d.ts +24 -1
- package/dist/lib/safe-config-write.d.ts.map +1 -1
- package/dist/lib/safe-config-write.js +113 -26
- package/dist/lib/safe-config-write.js.map +1 -1
- package/dist/lib/scope-real.d.ts.map +1 -1
- package/dist/lib/search-query.d.ts +21 -0
- package/dist/lib/search-query.d.ts.map +1 -1
- package/dist/lib/search-query.js +1 -1
- package/dist/lib/search-query.js.map +1 -1
- package/dist/lib/search-rerank.d.ts +23 -0
- package/dist/lib/search-rerank.d.ts.map +1 -0
- package/dist/lib/search-rerank.js +63 -0
- package/dist/lib/search-rerank.js.map +1 -0
- package/dist/lib/suppress-sqlite-warning.d.ts +13 -0
- package/dist/lib/suppress-sqlite-warning.d.ts.map +1 -0
- package/dist/lib/suppress-sqlite-warning.js +21 -0
- package/dist/lib/suppress-sqlite-warning.js.map +1 -0
- package/dist/lib/sync/real-env.d.ts.map +1 -1
- package/dist/lib/sync/spawn-background.d.ts +1 -1
- package/dist/lib/sync/spawn-background.d.ts.map +1 -1
- package/dist/lib/sync/spawn-background.js +1 -1
- package/dist/lib/sync/spawn-background.js.map +1 -1
- package/dist/lib/tar-lite.d.ts +18 -0
- package/dist/lib/tar-lite.d.ts.map +1 -0
- package/dist/lib/tar-lite.js +120 -0
- package/dist/lib/tar-lite.js.map +1 -0
- package/dist/lib/user-hooks.d.ts.map +1 -1
- package/dist/lib/user-hooks.js +13 -3
- package/dist/lib/user-hooks.js.map +1 -1
- package/dist/lib/verified-download.d.ts +19 -0
- package/dist/lib/verified-download.d.ts.map +1 -0
- package/dist/lib/verified-download.js +105 -0
- package/dist/lib/verified-download.js.map +1 -0
- package/package.json +6 -6
package/dist/lib/model-cache.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { createHash } from 'node:crypto';
|
|
6
6
|
import { existsSync, readFileSync } from 'node:fs';
|
|
7
|
+
import { homedir } from 'node:os';
|
|
7
8
|
import { join } from 'node:path';
|
|
8
9
|
import { ensurePrivateDir, writePrivateFile } from './private-files.js';
|
|
9
10
|
/**
|
|
@@ -32,4 +33,29 @@ export function modelFileCache(dir) {
|
|
|
32
33
|
},
|
|
33
34
|
};
|
|
34
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Where everything the on-device embedding path caches lives: the model files and the native
|
|
38
|
+
* runtime, for the npm install and the standalone binary alike (one directory to warm in CI,
|
|
39
|
+
* one to delete to start clean).
|
|
40
|
+
*
|
|
41
|
+
* Never inside the install tree. transformers.js defaulted to `node_modules/@huggingface/
|
|
42
|
+
* transformers/.cache`, which is read-only for a root-owned global prefix (`sudo npm i -g`),
|
|
43
|
+
* is wiped by every upgrade, and ignored XDG_CACHE_HOME.
|
|
44
|
+
*
|
|
45
|
+
* `ALIGN_MODEL_CACHE` overrides all of it. Otherwise XDG_CACHE_HOME (or ~/.cache), and
|
|
46
|
+
* %LOCALAPPDATA% on Windows.
|
|
47
|
+
*/
|
|
48
|
+
export function modelCacheDir(env = process.env, platform = process.platform, home = homedir()) {
|
|
49
|
+
// Absolute only. The cache holds code the CLI loads (the native runtime), so a relative value,
|
|
50
|
+
// which a repo's .env can supply, would make the current repo's files the thing that gets loaded.
|
|
51
|
+
const override = env['ALIGN_MODEL_CACHE'];
|
|
52
|
+
if (override && /^(\/|[A-Za-z]:[\\/]|\\\\)/.test(override))
|
|
53
|
+
return override;
|
|
54
|
+
if (platform === 'win32') {
|
|
55
|
+
const base = env['LOCALAPPDATA'] || join(home, 'AppData', 'Local');
|
|
56
|
+
return join(base, 'align-cli', 'models');
|
|
57
|
+
}
|
|
58
|
+
const base = env['XDG_CACHE_HOME'] || join(home, '.cache');
|
|
59
|
+
return join(base, 'align-cli', 'models');
|
|
60
|
+
}
|
|
35
61
|
//# sourceMappingURL=model-cache.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"model-cache.js","sourceRoot":"","sources":["../../src/lib/model-cache.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAExE;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,GAAW;IACxC,MAAM,MAAM,GAAG,CAAC,OAAgB,EAAU,EAAE;QAC1C,MAAM,GAAG,GAAG,OAAO,OAAO,KAAK,QAAQ;YACrC,CAAC,CAAC,OAAO;YACT,CAAC,CAAE,OAA4B,CAAC,GAAG,IAAI,MAAM,CAAC,OAAO,CAAC,CAAC;QACzD,OAAO,IAAI,CAAC,GAAG,EAAE,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;IAChF,CAAC,CAAC;IACF,OAAO;QACL,KAAK,CAAC,KAAK,CAAC,OAAgB;YAC1B,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC;YAC7B,OAAO,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,QAAQ,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QACzE,CAAC;QACD,KAAK,CAAC,GAAG,CAAC,OAAgB,EAAE,QAAkB;YAC5C,8FAA8F;YAC9F,oCAAoC;YACpC,gBAAgB,CAAC,GAAG,CAAC,CAAC;YACtB,wEAAwE;YACxE,gBAAgB,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,QAAQ,CAAC,KAAK,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;QACvF,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
1
|
+
{"version":3,"file":"model-cache.js","sourceRoot":"","sources":["../../src/lib/model-cache.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAExE;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,GAAW;IACxC,MAAM,MAAM,GAAG,CAAC,OAAgB,EAAU,EAAE;QAC1C,MAAM,GAAG,GAAG,OAAO,OAAO,KAAK,QAAQ;YACrC,CAAC,CAAC,OAAO;YACT,CAAC,CAAE,OAA4B,CAAC,GAAG,IAAI,MAAM,CAAC,OAAO,CAAC,CAAC;QACzD,OAAO,IAAI,CAAC,GAAG,EAAE,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;IAChF,CAAC,CAAC;IACF,OAAO;QACL,KAAK,CAAC,KAAK,CAAC,OAAgB;YAC1B,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC;YAC7B,OAAO,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,QAAQ,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QACzE,CAAC;QACD,KAAK,CAAC,GAAG,CAAC,OAAgB,EAAE,QAAkB;YAC5C,8FAA8F;YAC9F,oCAAoC;YACpC,gBAAgB,CAAC,GAAG,CAAC,CAAC;YACtB,wEAAwE;YACxE,gBAAgB,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,QAAQ,CAAC,KAAK,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;QACvF,CAAC;KACF,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,aAAa,CAAC,MAA0C,OAAO,CAAC,GAAG,EAAE,WAAmB,OAAO,CAAC,QAAQ,EAAE,OAAe,OAAO,EAAE;IAChJ,+FAA+F;IAC/F,kGAAkG;IAClG,MAAM,QAAQ,GAAG,GAAG,CAAC,mBAAmB,CAAC,CAAC;IAC1C,IAAI,QAAQ,IAAI,2BAA2B,CAAC,IAAI,CAAC,QAAQ,CAAC;QAAE,OAAO,QAAQ,CAAC;IAC5E,IAAI,QAAQ,KAAK,OAAO,EAAE,CAAC;QACzB,MAAM,IAAI,GAAG,GAAG,CAAC,cAAc,CAAC,IAAI,IAAI,CAAC,IAAI,EAAE,SAAS,EAAE,OAAO,CAAC,CAAC;QACnE,OAAO,IAAI,CAAC,IAAI,EAAE,WAAW,EAAE,QAAQ,CAAC,CAAC;IAC3C,CAAC;IACD,MAAM,IAAI,GAAG,GAAG,CAAC,gBAAgB,CAAC,IAAI,IAAI,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;IAC3D,OAAO,IAAI,CAAC,IAAI,EAAE,WAAW,EAAE,QAAQ,CAAC,CAAC;AAC3C,CAAC"}
|
package/dist/lib/next-step.d.ts
CHANGED
|
@@ -33,6 +33,8 @@ export declare function orderAgents(w: WiredAgents): string[];
|
|
|
33
33
|
export declare function projectAgentsFromWritten(written: string[]): string[];
|
|
34
34
|
/** Prints only when something was actually wired: a connection claim with nothing behind it is worse than silence. */
|
|
35
35
|
export declare function agentConnectedLine(agents: string[]): string | undefined;
|
|
36
|
+
/** The first title in the list that makes a grammatical question, as it was given. */
|
|
37
|
+
export declare function pickAskableTitle(titles: readonly string[]): string | undefined;
|
|
36
38
|
/**
|
|
37
39
|
* The question to hand the agent. Against a REAL decision the wizard found when there is one,
|
|
38
40
|
* because "why did we X" is checkable against the repo the user is sitting in. The fallback
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"next-step.d.ts","sourceRoot":"","sources":["../../src/lib/next-step.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAG3C;;;;;;;;;;;;GAYG;AAEH,MAAM,WAAW,WAAW;IAC1B,mEAAmE;IACnE,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,oEAAoE;IACpE,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,WAAW,GAAG,MAAM,EAAE,CASpD;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAEpE;AAED,sHAAsH;AACtH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,GAAG,SAAS,CAGvE;
|
|
1
|
+
{"version":3,"file":"next-step.d.ts","sourceRoot":"","sources":["../../src/lib/next-step.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAG3C;;;;;;;;;;;;GAYG;AAEH,MAAM,WAAW,WAAW;IAC1B,mEAAmE;IACnE,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,oEAAoE;IACpE,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,WAAW,GAAG,MAAM,EAAE,CASpD;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAEpE;AAED,sHAAsH;AACtH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,GAAG,SAAS,CAGvE;AAmCD,sFAAsF;AACtF,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,GAAG,SAAS,CAE9E;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAGpE;AAOD;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE;IACjC,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;CAClB,GAAG,MAAM,CAOT;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,eAAe,EAAE,MAAM,CAAC;IACxB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,MAAM,WAAW,aAAa;IAC5B,qFAAqF;IACrF,SAAS,EAAE,MAAM,CAAC;IAClB,mDAAmD;IACnD,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,YAAY,EAAE,OAAO,CAAC;IACtB,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,OAAO,EAAE,OAAO,CAAC;IACjB,oFAAoF;IACpF,KAAK,EAAE,SAAS,GAAG,SAAS,CAAC;CAC9B;AAID,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE/C;AAWD;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,aAAa,GAAG,MAAM,CAe/D"}
|
package/dist/lib/next-step.js
CHANGED
|
@@ -31,21 +31,43 @@ export function agentConnectedLine(agents) {
|
|
|
31
31
|
return `Your agent is connected: ${agents.join(', ')}.`;
|
|
32
32
|
}
|
|
33
33
|
/**
|
|
34
|
-
*
|
|
35
|
-
* phrase
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
34
|
+
* ALI-1779: a decision title is only quoted into "why did we X?" when X is a base-form verb
|
|
35
|
+
* phrase, so the result is a sentence. A raw commit subject is not one: "fix: error throwing in
|
|
36
|
+
* reply (#6299)" has a conventional prefix and a PR number, and "Improved links to MADR" is past
|
|
37
|
+
* tense. The prefix and PR number are stripped, and the rest must open with a verb from the list
|
|
38
|
+
* below; anything else (noun phrases, past tense, a leftover colon, policy statements such as
|
|
39
|
+
* "All PRs must ...", over twelve words) falls back to the generic question. The list is
|
|
40
|
+
* deliberately short and quoting a subject is optional, so a miss costs only a generic question.
|
|
40
41
|
*/
|
|
41
|
-
const
|
|
42
|
+
const CONVENTIONAL_PREFIX = /^(?:feat|fix|refactor|perf|types?|deps?|security)(?:\([^)]*\))?!?:\s*/i;
|
|
43
|
+
const PR_REFERENCE = /\s*\(#\d+[^)]*\)/g;
|
|
42
44
|
const MAX_TOPIC_WORDS = 12;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
const BASE_VERBS = new Set([
|
|
46
|
+
'add', 'adopt', 'allow', 'avoid', 'change', 'close', 'consolidate', 'delegate', 'deprecate',
|
|
47
|
+
'disable', 'drop', 'enable', 'enforce', 'ensure', 'extract', 'fix', 'handle', 'ignore', 'implement',
|
|
48
|
+
'introduce', 'keep', 'make', 'migrate', 'move', 'prefer', 'prevent', 'put', 'refactor',
|
|
49
|
+
'reject', 'remove', 'rename', 'replace', 'require', 'restrict', 'reuse', 'send',
|
|
50
|
+
'simplify', 'skip', 'split', 'stop', 'switch', 'throw', 'treat', 'update',
|
|
51
|
+
'upgrade', 'use', 'validate', 'warn',
|
|
52
|
+
]);
|
|
53
|
+
/** The quotable phrase inside a commit subject or decision title, or undefined. */
|
|
54
|
+
function askablePhrase(title) {
|
|
55
|
+
if (!title)
|
|
56
|
+
return undefined;
|
|
57
|
+
const phrase = title.trim().replace(CONVENTIONAL_PREFIX, '').replace(PR_REFERENCE, '').trim().replace(/\.$/, '');
|
|
58
|
+
if (phrase === '' || /[:?;#]/.test(phrase))
|
|
59
|
+
return undefined;
|
|
60
|
+
const words = phrase.split(/\s+/);
|
|
61
|
+
if (words.length > MAX_TOPIC_WORDS)
|
|
62
|
+
return undefined;
|
|
63
|
+
const verb = words[0].toLowerCase();
|
|
64
|
+
if (!BASE_VERBS.has(verb))
|
|
65
|
+
return undefined;
|
|
66
|
+
return verb + phrase.slice(words[0].length);
|
|
67
|
+
}
|
|
68
|
+
/** The first title in the list that makes a grammatical question, as it was given. */
|
|
69
|
+
export function pickAskableTitle(titles) {
|
|
70
|
+
return titles.find((t) => askablePhrase(t) !== undefined);
|
|
49
71
|
}
|
|
50
72
|
/**
|
|
51
73
|
* The question to hand the agent. Against a REAL decision the wizard found when there is one,
|
|
@@ -55,9 +77,8 @@ function looksLikeTopic(title) {
|
|
|
55
77
|
* list tool rather than a similarity search.
|
|
56
78
|
*/
|
|
57
79
|
export function firstQuestion(firstTitle) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
: 'what decisions exist in this codebase?';
|
|
80
|
+
const phrase = askablePhrase(firstTitle);
|
|
81
|
+
return phrase ? `why did we ${phrase}?` : 'what decisions exist in this codebase?';
|
|
61
82
|
}
|
|
62
83
|
/** Runnable AS PRINTED: bare `align mcp --setup` wires the cloud default, which is not a local user's graph. */
|
|
63
84
|
function mcpSetupCommand(envName) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"next-step.js","sourceRoot":"","sources":["../../src/lib/next-step.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAC;AAuBhD;;;;GAIG;AACH,MAAM,UAAU,WAAW,CAAC,CAAc;IACxC,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,KAAK,MAAM,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC;QAC/C,IAAI,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC;YAAE,SAAS;QAC7B,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACf,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACjB,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,wBAAwB,CAAC,OAAiB;IACxD,OAAO,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;AAC9D,CAAC;AAED,sHAAsH;AACtH,MAAM,UAAU,kBAAkB,CAAC,MAAgB;IACjD,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IAC1C,OAAO,4BAA4B,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC;AAC1D,CAAC;AAED
|
|
1
|
+
{"version":3,"file":"next-step.js","sourceRoot":"","sources":["../../src/lib/next-step.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What to tell a person on a Node older than the floor, or null when the runtime is fine.
|
|
3
|
+
* Pure, and it must stay free of imports that reach node:sqlite: it runs before the CLI loads.
|
|
4
|
+
*/
|
|
5
|
+
export declare function nodeFloorMessage(version: string): string | null;
|
|
6
|
+
//# sourceMappingURL=node-floor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"node-floor.d.ts","sourceRoot":"","sources":["../../src/lib/node-floor.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAO/D"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { MIN_NODE, nodeMeetsFloor } from './doctor.js';
|
|
2
|
+
/**
|
|
3
|
+
* What to tell a person on a Node older than the floor, or null when the runtime is fine.
|
|
4
|
+
* Pure, and it must stay free of imports that reach node:sqlite: it runs before the CLI loads.
|
|
5
|
+
*/
|
|
6
|
+
export function nodeFloorMessage(version) {
|
|
7
|
+
if (nodeMeetsFloor(version))
|
|
8
|
+
return null;
|
|
9
|
+
return [
|
|
10
|
+
`align needs Node ${MIN_NODE} or newer, and this is Node ${version}.`,
|
|
11
|
+
'Install a current Node from https://nodejs.org, open a new terminal, and run align again.',
|
|
12
|
+
'No Node to hand? The binary needs none: curl -fsSL https://align.tech/install.sh | sh',
|
|
13
|
+
].join('\n');
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=node-floor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"node-floor.js","sourceRoot":"","sources":["../../src/lib/node-floor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAEvD;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAAC,OAAe;IAC9C,IAAI,cAAc,CAAC,OAAO,CAAC;QAAE,OAAO,IAAI,CAAC;IACzC,OAAO;QACL,oBAAoB,QAAQ,+BAA+B,OAAO,GAAG;QACrE,2FAA2F;QAC3F,uFAAuF;KACxF,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { chmodSync, copyFileSync, lstatSync, mkdirSync, readdirSync, readFileSync, readlinkSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
1
|
+
import { chmodSync, copyFileSync, lstatSync, mkdirSync, readdirSync, readFileSync, readlinkSync, renameSync, rmdirSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
2
2
|
export declare const BACKUP_SUFFIX = ".align-backup";
|
|
3
3
|
export type SafeWriteStatus = 'written' | 'unchanged' | 'declined' | 'symlink';
|
|
4
4
|
/** The fs calls the writer makes. A parameter so a test can change the file mid-write. */
|
|
@@ -13,6 +13,7 @@ export interface SafeFs {
|
|
|
13
13
|
unlinkSync: typeof unlinkSync;
|
|
14
14
|
mkdirSync: typeof mkdirSync;
|
|
15
15
|
readdirSync: typeof readdirSync;
|
|
16
|
+
rmdirSync: typeof rmdirSync;
|
|
16
17
|
}
|
|
17
18
|
type Json = Record<string, unknown>;
|
|
18
19
|
/** One value or array element align put in a JSON config. Hashes only, never the value. */
|
|
@@ -48,6 +49,11 @@ export interface WrittenConfig {
|
|
|
48
49
|
/** 'made': align made the backup. 'foreign': one was already there and is not trusted. 'none': no original to keep. */
|
|
49
50
|
backup: 'made' | 'foreign' | 'none';
|
|
50
51
|
backupSha256?: string;
|
|
52
|
+
/**
|
|
53
|
+
* Where the backup lives when it is not beside the file (project files). Snapshots are
|
|
54
|
+
* `<backupAt>.<n>`. Absent: `<file>.align-backup`.
|
|
55
|
+
*/
|
|
56
|
+
backupAt?: string;
|
|
51
57
|
owned?: OwnedItem[];
|
|
52
58
|
/**
|
|
53
59
|
* Extra copies of the file taken just before a write that found it changed by someone else
|
|
@@ -57,6 +63,11 @@ export interface WrittenConfig {
|
|
|
57
63
|
n: number;
|
|
58
64
|
sha256: string;
|
|
59
65
|
}>;
|
|
66
|
+
/**
|
|
67
|
+
* Directories align's first write made on the way to this file (outermost first). Undo removes
|
|
68
|
+
* each one only if it is empty by then, so a directory that held anything else stays.
|
|
69
|
+
*/
|
|
70
|
+
createdDirs?: string[];
|
|
60
71
|
/** Text files: the managed block align wrote. */
|
|
61
72
|
block?: {
|
|
62
73
|
start: string;
|
|
@@ -98,12 +109,24 @@ export interface SafeWriteOptions {
|
|
|
98
109
|
};
|
|
99
110
|
/** TOML only: the table (e.g. ['mcp_servers', 'align-local']) the managed block holds. */
|
|
100
111
|
tomlTable?: string[];
|
|
112
|
+
/**
|
|
113
|
+
* A file in the user's repo, not in an agent's private config: new files and directories get the
|
|
114
|
+
* default mode (the umask decides) instead of owner-only, since a team shares them.
|
|
115
|
+
*/
|
|
116
|
+
projectFile?: boolean;
|
|
101
117
|
/** The tail of the invalid-JSON message: what to do about it. */
|
|
102
118
|
invalidJsonAdvice?: string;
|
|
103
119
|
}
|
|
104
120
|
export declare class SafeWriteConflictError extends Error {
|
|
105
121
|
constructor(file: string);
|
|
106
122
|
}
|
|
123
|
+
/**
|
|
124
|
+
* Where a PROJECT file's backup goes: `<state home>/align-cli/undo-backups/<sha of the full path>/<name>.align-backup`.
|
|
125
|
+
* Out of the repo on purpose, so nothing shows in `git status`. Keyed on the full path, the same key
|
|
126
|
+
* the ledger uses, so a moved repo loses both together and a same-named file elsewhere never collides.
|
|
127
|
+
* XDG_STATE_HOME is honoured; %LOCALAPPDATA% on Windows (case-folded there, as its paths are).
|
|
128
|
+
*/
|
|
129
|
+
export declare function projectBackupBase(file: string): string;
|
|
107
130
|
/** The text form. `compute` gets the current text (null when missing) and returns the new text, or undefined to leave it. */
|
|
108
131
|
export declare function safeWriteText(file: string, compute: (current: string | null) => string | undefined, opts?: SafeWriteOptions): SafeWriteStatus;
|
|
109
132
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"safe-config-write.d.ts","sourceRoot":"","sources":["../../src/lib/safe-config-write.ts"],"names":[],"mappings":"AACA,OAAO,EACL,SAAS,EACT,YAAY,EACZ,SAAS,EACT,SAAS,EACT,WAAW,EACX,YAAY,EACZ,YAAY,EACZ,UAAU,EACV,UAAU,EACV,aAAa,EACd,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"safe-config-write.d.ts","sourceRoot":"","sources":["../../src/lib/safe-config-write.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { createHash, randomBytes } from 'node:crypto';
|
|
2
|
-
import { chmodSync, copyFileSync, lstatSync, mkdirSync, readdirSync, readFileSync, readlinkSync, renameSync, unlinkSync, writeFileSync, } from 'node:fs';
|
|
2
|
+
import { chmodSync, copyFileSync, lstatSync, mkdirSync, readdirSync, readFileSync, readlinkSync, renameSync, rmdirSync, unlinkSync, writeFileSync, } from 'node:fs';
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { parse as parseToml } from 'smol-toml';
|
|
6
|
+
import { stateHomeFor } from './backfill-state.js';
|
|
6
7
|
/*
|
|
7
8
|
* The one way align writes to a config file that belongs to someone else (an agent's MCP
|
|
8
9
|
* config, its hook file). Promises, each of which a plain writeFileSync breaks:
|
|
9
|
-
* 1. The FIRST content is kept in `<file>.align-backup` and never overwritten.
|
|
10
|
+
* 1. The FIRST content is kept in `<file>.align-backup` and never overwritten. A PROJECT file
|
|
11
|
+
* (a committed AGENTS.md) is the exception: its copy goes under the user's state directory, so
|
|
12
|
+
* `git status` shows nothing of it and `git add -A` cannot commit it.
|
|
10
13
|
* 2. The file is re-read after our new content is staged. If another program changed it in
|
|
11
14
|
* the meantime we recompute once from what is there now, and give up if it moves again.
|
|
12
15
|
* 3. The new content lands by an exclusive temp file + rename, keeping the old file's mode.
|
|
@@ -18,7 +21,7 @@ import { parse as parseToml } from 'smol-toml';
|
|
|
18
21
|
export const BACKUP_SUFFIX = '.align-backup';
|
|
19
22
|
// Built on use, not at import: a test that mocks node:fs with a few names must still be able
|
|
20
23
|
// to import anything that reaches this module.
|
|
21
|
-
const realFs = () => ({ readFileSync, writeFileSync, renameSync, copyFileSync, lstatSync, readlinkSync, chmodSync, unlinkSync, mkdirSync, readdirSync });
|
|
24
|
+
const realFs = () => ({ readFileSync, writeFileSync, renameSync, copyFileSync, lstatSync, readlinkSync, chmodSync, unlinkSync, mkdirSync, readdirSync, rmdirSync });
|
|
22
25
|
const isObject = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
23
26
|
const sha = (data) => createHash('sha256').update(data).digest('hex');
|
|
24
27
|
/** JSON with sorted keys, so a hash does not depend on key order. */
|
|
@@ -59,7 +62,9 @@ export function mergeWrittenConfig(prev, next) {
|
|
|
59
62
|
firstSha256: prev.firstSha256,
|
|
60
63
|
backup: prev.backup,
|
|
61
64
|
...(prev.backupSha256 ? { backupSha256: prev.backupSha256 } : {}),
|
|
65
|
+
...(prev.backupAt ? { backupAt: prev.backupAt } : {}),
|
|
62
66
|
...(owned.size > 0 ? { owned: [...owned.values()] } : {}),
|
|
67
|
+
...((prev.createdDirs?.length || next.createdDirs?.length) ? { createdDirs: [...new Set([...(prev.createdDirs ?? []), ...(next.createdDirs ?? [])])] } : {}),
|
|
63
68
|
...((prev.snapshots?.length || next.snapshots?.length) ? { snapshots: [...(prev.snapshots ?? []), ...(next.snapshots ?? [])] } : {}),
|
|
64
69
|
...(next.block
|
|
65
70
|
? { block: { ...next.block, ...(prev.block ? (prev.block.replaced ? { replaced: true } : {}) : next.block.replaced ? { replaced: true } : {}) } }
|
|
@@ -141,12 +146,15 @@ function modeOf(fs, file) {
|
|
|
141
146
|
}
|
|
142
147
|
}
|
|
143
148
|
/** 'made' only when this call created the backup. Anything already at that path is not ours. */
|
|
144
|
-
function backupOnce(fs, file) {
|
|
145
|
-
const backup = `${file}${BACKUP_SUFFIX}`;
|
|
149
|
+
function backupOnce(fs, file, backup, private_) {
|
|
146
150
|
if (linkAt(fs, backup) !== 'missing')
|
|
147
151
|
return { state: 'foreign' };
|
|
152
|
+
if (private_)
|
|
153
|
+
fs.mkdirSync(path.dirname(backup), { recursive: true, mode: 0o700 });
|
|
148
154
|
try {
|
|
149
155
|
fs.copyFileSync(file, backup, 1); // COPYFILE_EXCL
|
|
156
|
+
if (private_)
|
|
157
|
+
fs.chmodSync(backup, 0o600);
|
|
150
158
|
}
|
|
151
159
|
catch (err) {
|
|
152
160
|
if (err.code === 'EEXIST')
|
|
@@ -160,9 +168,11 @@ function backupOnce(fs, file) {
|
|
|
160
168
|
* last write: the backup from before align's first write cannot hold what they added since.
|
|
161
169
|
* `<file>.align-backup.<n>`, exclusive, 0600 (it may hold secrets), never overwritten.
|
|
162
170
|
*/
|
|
163
|
-
function takeSnapshot(fs, file, content, taken) {
|
|
171
|
+
function takeSnapshot(fs, file, base, content, taken) {
|
|
172
|
+
if (base !== `${file}${BACKUP_SUFFIX}`)
|
|
173
|
+
fs.mkdirSync(path.dirname(base), { recursive: true, mode: 0o700 });
|
|
164
174
|
for (let n = Math.max(0, ...taken) + 1; n <= MAX_SNAPSHOTS; n++) {
|
|
165
|
-
const target = `${
|
|
175
|
+
const target = `${base}.${n}`;
|
|
166
176
|
if (linkAt(fs, target) !== 'missing')
|
|
167
177
|
continue;
|
|
168
178
|
try {
|
|
@@ -178,9 +188,9 @@ function takeSnapshot(fs, file, content, taken) {
|
|
|
178
188
|
throw new Error(`${file} already has ${taken.length} snapshots from align. Run \`align use --undo\` to clear them, then try again.`);
|
|
179
189
|
}
|
|
180
190
|
/** Delete snapshots THIS call made (verified: a regular file whose hash is the one taken). */
|
|
181
|
-
function dropSnapshots(fs,
|
|
191
|
+
function dropSnapshots(fs, base, list) {
|
|
182
192
|
for (const { n, sha256: want } of list) {
|
|
183
|
-
const target = `${
|
|
193
|
+
const target = `${base}.${n}`;
|
|
184
194
|
try {
|
|
185
195
|
if (fs.lstatSync(target).isFile() && sha(fs.readFileSync(target)) === want)
|
|
186
196
|
fs.unlinkSync(target);
|
|
@@ -190,6 +200,17 @@ function dropSnapshots(fs, file, list) {
|
|
|
190
200
|
}
|
|
191
201
|
}
|
|
192
202
|
}
|
|
203
|
+
/**
|
|
204
|
+
* Where a PROJECT file's backup goes: `<state home>/align-cli/undo-backups/<sha of the full path>/<name>.align-backup`.
|
|
205
|
+
* Out of the repo on purpose, so nothing shows in `git status`. Keyed on the full path, the same key
|
|
206
|
+
* the ledger uses, so a moved repo loses both together and a same-named file elsewhere never collides.
|
|
207
|
+
* XDG_STATE_HOME is honoured; %LOCALAPPDATA% on Windows (case-folded there, as its paths are).
|
|
208
|
+
*/
|
|
209
|
+
export function projectBackupBase(file) {
|
|
210
|
+
const abs = path.resolve(file);
|
|
211
|
+
const key = sha(process.platform === 'win32' ? abs.toLowerCase() : abs).slice(0, 24);
|
|
212
|
+
return path.join(stateHomeFor(process.env, os.homedir(), process.platform), 'align-cli', 'undo-backups', key, `${path.basename(file)}${BACKUP_SUFFIX}`);
|
|
213
|
+
}
|
|
193
214
|
const MAX_SNAPSHOTS = 200;
|
|
194
215
|
const STALE_TMP_MS = 60_000;
|
|
195
216
|
/** A crash between staging and rename leaves `.<name>.<hex>.align-tmp`. Old ones are removed. */
|
|
@@ -211,12 +232,12 @@ function sweepStaleTemps(fs, file) {
|
|
|
211
232
|
}
|
|
212
233
|
}
|
|
213
234
|
/** Stage `text` beside `file` and rename it over. Never leaves the temp file behind on failure. */
|
|
214
|
-
function stageAndRename(fs, file, text, mode, beforeRename) {
|
|
235
|
+
function stageAndRename(fs, file, text, mode, beforeRename, newMode = 0o600) {
|
|
215
236
|
let tmp = '';
|
|
216
237
|
for (let i = 0; i < 5; i++) {
|
|
217
238
|
tmp = path.join(path.dirname(file), `.${path.basename(file)}.${randomBytes(4).toString('hex')}.align-tmp`);
|
|
218
239
|
try {
|
|
219
|
-
fs.writeFileSync(tmp, text, { encoding: 'utf8', mode: mode ??
|
|
240
|
+
fs.writeFileSync(tmp, text, { encoding: 'utf8', mode: mode ?? newMode, flag: 'wx' });
|
|
220
241
|
break;
|
|
221
242
|
}
|
|
222
243
|
catch (err) {
|
|
@@ -360,6 +381,22 @@ function blockOf(before, next, markers, tomlTable) {
|
|
|
360
381
|
}
|
|
361
382
|
return { start: markers.start, end: markers.end, sha256: sha(region), ...(old !== null && old !== region ? { replaced: true } : {}), ...(table ? { table } : {}) };
|
|
362
383
|
}
|
|
384
|
+
/** The directories on the way to `dir` that do not exist yet, outermost first. */
|
|
385
|
+
function missingDirs(fs, dir) {
|
|
386
|
+
const missing = [];
|
|
387
|
+
for (let d = dir;; d = path.dirname(d)) {
|
|
388
|
+
try {
|
|
389
|
+
fs.lstatSync(d);
|
|
390
|
+
break;
|
|
391
|
+
}
|
|
392
|
+
catch {
|
|
393
|
+
missing.unshift(d);
|
|
394
|
+
}
|
|
395
|
+
if (path.dirname(d) === d)
|
|
396
|
+
break;
|
|
397
|
+
}
|
|
398
|
+
return missing;
|
|
399
|
+
}
|
|
363
400
|
function safeWrite(file, compute, opts, describe) {
|
|
364
401
|
const fs = opts.fs ?? realFs();
|
|
365
402
|
const note = opts.note ?? ((l) => console.error(l));
|
|
@@ -371,7 +408,11 @@ function safeWrite(file, compute, opts, describe) {
|
|
|
371
408
|
return 'symlink';
|
|
372
409
|
}
|
|
373
410
|
let backup = { state: 'none' };
|
|
411
|
+
// A file already recorded keeps the place its first backup went; a new project file gets a place outside the repo.
|
|
412
|
+
const backupAt = lookup?.(file)?.backupAt ?? (opts.projectFile ? projectBackupBase(file) : undefined);
|
|
413
|
+
const backupBase = backupAt ?? `${file}${BACKUP_SUFFIX}`;
|
|
374
414
|
const snapshots = [];
|
|
415
|
+
let createdDirs = [];
|
|
375
416
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
376
417
|
const before = readCurrent(fs, file);
|
|
377
418
|
const next = compute(before);
|
|
@@ -379,7 +420,10 @@ function safeWrite(file, compute, opts, describe) {
|
|
|
379
420
|
return 'declined';
|
|
380
421
|
if (next === before)
|
|
381
422
|
return 'unchanged';
|
|
382
|
-
fs
|
|
423
|
+
const madeDirs = missingDirs(fs, path.dirname(file));
|
|
424
|
+
fs.mkdirSync(path.dirname(file), { recursive: true, mode: opts.projectFile ? 0o777 : 0o700 });
|
|
425
|
+
if (madeDirs.length > 0)
|
|
426
|
+
createdDirs = madeDirs;
|
|
383
427
|
sweepStaleTemps(fs, file);
|
|
384
428
|
const mode = modeOf(fs, file);
|
|
385
429
|
// The first time align writes a file that exists, the original is kept. After that, if the file
|
|
@@ -389,18 +433,18 @@ function safeWrite(file, compute, opts, describe) {
|
|
|
389
433
|
if (before !== null) {
|
|
390
434
|
if (!prev) {
|
|
391
435
|
if (backup.state === 'none')
|
|
392
|
-
backup = backupOnce(fs, file);
|
|
436
|
+
backup = backupOnce(fs, file, backupBase, backupAt !== undefined);
|
|
393
437
|
}
|
|
394
438
|
else if (sha(before) !== prev.sha256 && !snapshots.some((x) => x.sha256 === sha(before))) {
|
|
395
|
-
snapshots.push(takeSnapshot(fs, file, before, [...(prev.snapshots ?? []).map((x) => x.n), ...snapshots.map((x) => x.n)]));
|
|
439
|
+
snapshots.push(takeSnapshot(fs, file, backupBase, before, [...(prev.snapshots ?? []).map((x) => x.n), ...snapshots.map((x) => x.n)]));
|
|
396
440
|
}
|
|
397
441
|
}
|
|
398
442
|
let staged = false;
|
|
399
443
|
try {
|
|
400
|
-
staged = stageAndRename(fs, file, next, mode, () => readCurrent(fs, file) === before);
|
|
444
|
+
staged = stageAndRename(fs, file, next, mode, () => readCurrent(fs, file) === before, opts.projectFile ? 0o666 : 0o600);
|
|
401
445
|
}
|
|
402
446
|
catch (err) {
|
|
403
|
-
dropSnapshots(fs,
|
|
447
|
+
dropSnapshots(fs, backupBase, snapshots);
|
|
404
448
|
throw err;
|
|
405
449
|
}
|
|
406
450
|
if (!staged)
|
|
@@ -423,7 +467,7 @@ function safeWrite(file, compute, opts, describe) {
|
|
|
423
467
|
...(block?.replaced ? [block.snapshot ?? 0] : []),
|
|
424
468
|
]);
|
|
425
469
|
const kept = snapshots.filter((x) => referenced.has(x.n));
|
|
426
|
-
dropSnapshots(fs,
|
|
470
|
+
dropSnapshots(fs, backupBase, snapshots.filter((x) => !referenced.has(x.n)));
|
|
427
471
|
try {
|
|
428
472
|
recorder?.(file, {
|
|
429
473
|
created: before === null,
|
|
@@ -431,7 +475,9 @@ function safeWrite(file, compute, opts, describe) {
|
|
|
431
475
|
firstSha256: sha(next),
|
|
432
476
|
backup: backup.state,
|
|
433
477
|
...(backup.sha256 ? { backupSha256: backup.sha256 } : {}),
|
|
478
|
+
...(backupAt ? { backupAt } : {}),
|
|
434
479
|
...(kept.length > 0 ? { snapshots: kept } : {}),
|
|
480
|
+
...(createdDirs.length > 0 ? { createdDirs } : {}),
|
|
435
481
|
...owned,
|
|
436
482
|
...(block ? { block } : {}),
|
|
437
483
|
});
|
|
@@ -442,7 +488,7 @@ function safeWrite(file, compute, opts, describe) {
|
|
|
442
488
|
}
|
|
443
489
|
return 'written';
|
|
444
490
|
}
|
|
445
|
-
dropSnapshots(fs,
|
|
491
|
+
dropSnapshots(fs, backupBase, snapshots);
|
|
446
492
|
throw new SafeWriteConflictError(file);
|
|
447
493
|
}
|
|
448
494
|
/** The text form. `compute` gets the current text (null when missing) and returns the new text, or undefined to leave it. */
|
|
@@ -492,12 +538,20 @@ export function ownedOf(before, after, prev = [], snapshot = 0) {
|
|
|
492
538
|
return !ownsElement(here, h);
|
|
493
539
|
});
|
|
494
540
|
};
|
|
541
|
+
/**
|
|
542
|
+
* A container that is already in the file but that align's own earlier write made: its
|
|
543
|
+
* createdDepth, so an entry added to it now is still pruned with it. null when it was the user's.
|
|
544
|
+
*/
|
|
545
|
+
const madeByAlign = (here) => {
|
|
546
|
+
const depths = prev.filter((i) => i.path.length > here.length && samePath(i.path.slice(0, here.length), here) && i.createdDepth <= here.length).map((i) => i.createdDepth);
|
|
547
|
+
return depths.length > 0 ? Math.min(...depths) : null;
|
|
548
|
+
};
|
|
495
549
|
const walk = (b, a, p, created) => {
|
|
496
550
|
for (const [k, av] of Object.entries(a)) {
|
|
497
551
|
const here = [...p, k];
|
|
498
552
|
const has = b !== undefined && Object.prototype.hasOwnProperty.call(b, k);
|
|
499
553
|
const bv = has ? b[k] : undefined;
|
|
500
|
-
const createdAt = created ?? (has ?
|
|
554
|
+
const createdAt = created ?? (has ? madeByAlign(here) : here.length);
|
|
501
555
|
if (has && shaOf(av) === shaOf(bv))
|
|
502
556
|
continue;
|
|
503
557
|
if (isObject(av) && Object.keys(av).length > 0 && here.length < 2 && (!has || isObject(bv))) {
|
|
@@ -662,17 +716,13 @@ function removeOwned(cur, owned, originalOf, backupPath) {
|
|
|
662
716
|
export function undoWrittenConfigs(manifest, fs = realFs()) {
|
|
663
717
|
const report = { restored: [], removed: [], cleaned: [], skipped: [], done: [] };
|
|
664
718
|
for (const [file, entry] of Object.entries(manifest)) {
|
|
665
|
-
const backupPath = `${file}${BACKUP_SUFFIX}`;
|
|
719
|
+
const backupPath = entry.backupAt ?? `${file}${BACKUP_SUFFIX}`;
|
|
666
720
|
const link = linkAt(fs, file);
|
|
667
721
|
if (link && link !== 'missing') {
|
|
668
722
|
report.skipped.push(`${file}: it is a symlink to ${link.target}, left alone`);
|
|
669
723
|
continue;
|
|
670
724
|
}
|
|
671
725
|
const cur = readCurrent(fs, file);
|
|
672
|
-
if (cur === null) {
|
|
673
|
-
report.done.push(file); // already gone: nothing of align's is left
|
|
674
|
-
continue;
|
|
675
|
-
}
|
|
676
726
|
// Compare-and-swap: the agent may rewrite its own file (Gemini saves settings) between the read
|
|
677
727
|
// above and our write. Re-read right before every destructive step; if it moved, touch nothing.
|
|
678
728
|
const moved = () => {
|
|
@@ -704,6 +754,17 @@ export function undoWrittenConfigs(manifest, fs = realFs()) {
|
|
|
704
754
|
}
|
|
705
755
|
};
|
|
706
756
|
const backupTrusted = () => snapText(0) !== null;
|
|
757
|
+
/** Directories align made for this file, innermost first, each only while empty. */
|
|
758
|
+
const dropDirs = () => {
|
|
759
|
+
for (const d of [...(entry.createdDirs ?? [])].reverse()) {
|
|
760
|
+
try {
|
|
761
|
+
fs.rmdirSync(d);
|
|
762
|
+
}
|
|
763
|
+
catch {
|
|
764
|
+
break; /* not empty (or gone): the outer ones hold it too */
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
};
|
|
707
768
|
/** Only after a fully clean undo, and only copies that are still align's own. */
|
|
708
769
|
const dropBackup = () => {
|
|
709
770
|
for (const n of [0, ...(entry.snapshots ?? []).map((x) => x.n)]) {
|
|
@@ -713,13 +774,29 @@ export function undoWrittenConfigs(manifest, fs = realFs()) {
|
|
|
713
774
|
}
|
|
714
775
|
catch { /* gone */ }
|
|
715
776
|
}
|
|
777
|
+
// The directory holding an out-of-repo copy: removed only while empty.
|
|
778
|
+
if (entry.backupAt) {
|
|
779
|
+
try {
|
|
780
|
+
fs.rmdirSync(path.dirname(entry.backupAt));
|
|
781
|
+
}
|
|
782
|
+
catch { /* not empty, or gone */ }
|
|
783
|
+
}
|
|
716
784
|
};
|
|
785
|
+
if (cur === null) {
|
|
786
|
+
dropBackup(); // already gone: nothing of align's is left, and its saved copy must not linger as a stale 'foreign' one
|
|
787
|
+
report.done.push(file);
|
|
788
|
+
continue;
|
|
789
|
+
}
|
|
717
790
|
// Whole-file only when align wrote this file exactly once and nobody has touched it since.
|
|
718
|
-
|
|
791
|
+
// A file align wrote WHOLE (no block, no owned keys: a generated plugin or rule) and created is all
|
|
792
|
+
// align's even after a re-run with other content, as long as it still equals the last write.
|
|
793
|
+
const wholeAlign = entry.created && !entry.block && !(entry.owned && entry.owned.length > 0);
|
|
794
|
+
if (sha(cur) === entry.sha256 && (entry.firstSha256 === entry.sha256 || wholeAlign)) {
|
|
719
795
|
if (entry.created) {
|
|
720
796
|
if (moved())
|
|
721
797
|
continue;
|
|
722
798
|
fs.unlinkSync(file);
|
|
799
|
+
dropDirs();
|
|
723
800
|
report.removed.push(file);
|
|
724
801
|
report.done.push(file);
|
|
725
802
|
continue;
|
|
@@ -741,6 +818,14 @@ export function undoWrittenConfigs(manifest, fs = realFs()) {
|
|
|
741
818
|
continue;
|
|
742
819
|
}
|
|
743
820
|
}
|
|
821
|
+
// The file is exactly what align wrote, so only the original is missing. A copy kept outside the repo
|
|
822
|
+
// can go missing (a cleaned state directory, a moved repo): say so and leave the file, rather than
|
|
823
|
+
// guess at what to take out. The record stays, so a restored copy lets undo run again.
|
|
824
|
+
const needsOriginal = !!entry.block?.replaced || (entry.owned ?? []).some((i) => i.replaced || i.removed) || (!entry.block && !(entry.owned && entry.owned.length > 0));
|
|
825
|
+
if (entry.backupAt && !entry.created && entry.backup === 'made' && !backupTrusted() && needsOriginal && sha(cur) === entry.sha256 && entry.firstSha256 === entry.sha256) {
|
|
826
|
+
report.skipped.push(`${file}: align's saved copy of the original (${backupPath}) is missing or changed, so align left the file as it is. Take align's lines out by hand`);
|
|
827
|
+
continue;
|
|
828
|
+
}
|
|
744
829
|
// Surgical: only align's own entries, and the user's own values back where align overwrote them.
|
|
745
830
|
const mention = entry.backup === 'foreign' ? `; ${backupPath} was not made by align, so it was not used` : '';
|
|
746
831
|
let next;
|
|
@@ -834,11 +919,12 @@ export function undoWrittenConfigs(manifest, fs = realFs()) {
|
|
|
834
919
|
}
|
|
835
920
|
// A file align created that holds nothing but align's own content once that is out: remove it, as
|
|
836
921
|
// an untouched one is.
|
|
837
|
-
if (entry.created && entry.owned && !entry.block && next.trim() === '{}') {
|
|
922
|
+
if (entry.created && ((entry.owned && !entry.block && next.trim() === '{}') || (entry.block && next.trim() === ''))) {
|
|
838
923
|
if (moved())
|
|
839
924
|
continue;
|
|
840
925
|
fs.unlinkSync(file);
|
|
841
926
|
dropBackup();
|
|
927
|
+
dropDirs();
|
|
842
928
|
report.removed.push(file);
|
|
843
929
|
report.done.push(file);
|
|
844
930
|
continue;
|
|
@@ -846,6 +932,7 @@ export function undoWrittenConfigs(manifest, fs = realFs()) {
|
|
|
846
932
|
if (next !== cur && !stageAndRename(fs, file, next, modeOf(fs, file), () => !moved()))
|
|
847
933
|
continue;
|
|
848
934
|
dropBackup();
|
|
935
|
+
dropDirs();
|
|
849
936
|
report.cleaned.push(file);
|
|
850
937
|
report.done.push(file);
|
|
851
938
|
}
|