architectonic 0.0.4 → 0.0.5
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 +38 -3
- package/bin/architectonic.js +477 -34
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,8 +12,9 @@ skills -- reusable procedures and capabilities
|
|
|
12
12
|
```
|
|
13
13
|
|
|
14
14
|
The initial placeholder releases were intentionally minimal. Starting in
|
|
15
|
-
`0.0.
|
|
16
|
-
|
|
15
|
+
`0.0.5`, `architectonic` can initialize a workspace, add layers, inspect the
|
|
16
|
+
local manifest, repair light manifest drift, update safely, and remove layers
|
|
17
|
+
without trampling local forks.
|
|
17
18
|
|
|
18
19
|
## Command shape
|
|
19
20
|
|
|
@@ -25,9 +26,11 @@ architectonic add identity
|
|
|
25
26
|
architectonic add project
|
|
26
27
|
architectonic add skills
|
|
27
28
|
architectonic add teleology identity skills
|
|
29
|
+
architectonic init
|
|
28
30
|
architectonic doctor
|
|
29
31
|
architectonic list
|
|
30
32
|
architectonic update
|
|
33
|
+
architectonic remove
|
|
31
34
|
```
|
|
32
35
|
|
|
33
36
|
`add` is explicit and leaves room for future verbs such as `doctor`, `list`,
|
|
@@ -45,8 +48,14 @@ npx architectonic add skills
|
|
|
45
48
|
npx architectonic add teleology identity skills
|
|
46
49
|
npx architectonic add skills --dir ./vendor
|
|
47
50
|
npx architectonic add teleology --source npm
|
|
51
|
+
npx architectonic init MyWorkspace
|
|
52
|
+
npx architectonic init --preset company
|
|
48
53
|
npx architectonic list
|
|
49
54
|
npx architectonic doctor
|
|
55
|
+
npx architectonic doctor --fix
|
|
56
|
+
npx architectonic update
|
|
57
|
+
npx architectonic update --dry-run
|
|
58
|
+
npx architectonic remove skills
|
|
50
59
|
```
|
|
51
60
|
|
|
52
61
|
`add` installs from the Architectonic GitHub organization into the current
|
|
@@ -87,7 +96,33 @@ ARCHITECTONIC_ADD_SOURCE # change the default source mode
|
|
|
87
96
|
`list` reads `architectonic.json` and shows installed layers.
|
|
88
97
|
|
|
89
98
|
`doctor` verifies that each recorded layer still exists and that the installed
|
|
90
|
-
package metadata matches the expected layer.
|
|
99
|
+
package metadata matches the expected layer. `doctor --fix` repairs light
|
|
100
|
+
manifest drift such as stale package names or recoverable default paths.
|
|
101
|
+
|
|
102
|
+
`init` creates a workspace root, installs a preset, and seeds a top-level
|
|
103
|
+
`README.md` and `AGENTS.md`.
|
|
104
|
+
|
|
105
|
+
Supported presets:
|
|
106
|
+
|
|
107
|
+
```text
|
|
108
|
+
solo # teleology + identity + project + skills
|
|
109
|
+
company # teleology + project + skills
|
|
110
|
+
project # project + skills
|
|
111
|
+
agent # identity + skills
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
`update` is conservative by design:
|
|
115
|
+
|
|
116
|
+
```text
|
|
117
|
+
git layers: only fast-forward clean git worktrees
|
|
118
|
+
npm layers: report newer packages but do not overwrite local forks
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
If a user has modified an installed instance, `update` should skip it rather
|
|
122
|
+
than flatten their divergence.
|
|
123
|
+
|
|
124
|
+
`remove` deletes a recorded layer and updates the manifest. If the layer is a
|
|
125
|
+
dirty git worktree, it refuses unless `--force` is explicit.
|
|
91
126
|
|
|
92
127
|
## Run vs install
|
|
93
128
|
|
package/bin/architectonic.js
CHANGED
|
@@ -4,7 +4,7 @@ import fs from "node:fs";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { spawnSync } from "node:child_process";
|
|
6
6
|
|
|
7
|
-
const VERSION = "0.0.
|
|
7
|
+
const VERSION = "0.0.5";
|
|
8
8
|
const args = process.argv.slice(2);
|
|
9
9
|
const [command, ...rest] = args;
|
|
10
10
|
const supported = ["teleology", "identity", "project", "skills"];
|
|
@@ -30,8 +30,12 @@ Usage:
|
|
|
30
30
|
npx architectonic add teleology identity skills
|
|
31
31
|
npx architectonic add skills --dir ./vendor
|
|
32
32
|
npx architectonic add teleology --source npm
|
|
33
|
+
npx architectonic init [name]
|
|
33
34
|
npx architectonic list
|
|
34
35
|
npx architectonic doctor
|
|
36
|
+
npx architectonic doctor --fix
|
|
37
|
+
npx architectonic update
|
|
38
|
+
npx architectonic remove <layer>
|
|
35
39
|
|
|
36
40
|
Layers:
|
|
37
41
|
teleology purpose, principles, doctrine, governance
|
|
@@ -49,6 +53,10 @@ What add does:
|
|
|
49
53
|
from git or npm sources and records them in architectonic.json.`);
|
|
50
54
|
}
|
|
51
55
|
|
|
56
|
+
function printUsageError(message) {
|
|
57
|
+
throw new Error(`${message}\nRun \`architectonic help\` for usage.`);
|
|
58
|
+
}
|
|
59
|
+
|
|
52
60
|
function parseAddArgs(tokens) {
|
|
53
61
|
const targets = [];
|
|
54
62
|
let installDir = process.cwd();
|
|
@@ -95,6 +103,153 @@ function parseAddArgs(tokens) {
|
|
|
95
103
|
return { targets, installDir, source };
|
|
96
104
|
}
|
|
97
105
|
|
|
106
|
+
function parseInitArgs(tokens) {
|
|
107
|
+
let installDir = process.cwd();
|
|
108
|
+
let source = process.env.ARCHITECTONIC_ADD_SOURCE || "git";
|
|
109
|
+
let preset = "solo";
|
|
110
|
+
let workspaceName = "";
|
|
111
|
+
|
|
112
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
113
|
+
const token = tokens[index];
|
|
114
|
+
if (token === "--dir" || token === "--out") {
|
|
115
|
+
const next = tokens[index + 1];
|
|
116
|
+
if (!next) {
|
|
117
|
+
throw new Error(`Missing value for ${token}`);
|
|
118
|
+
}
|
|
119
|
+
installDir = path.resolve(next);
|
|
120
|
+
index += 1;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (token.startsWith("--dir=")) {
|
|
124
|
+
installDir = path.resolve(token.slice("--dir=".length));
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (token.startsWith("--out=")) {
|
|
128
|
+
installDir = path.resolve(token.slice("--out=".length));
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (token === "--source") {
|
|
132
|
+
const next = tokens[index + 1];
|
|
133
|
+
if (!next) {
|
|
134
|
+
throw new Error("Missing value for --source");
|
|
135
|
+
}
|
|
136
|
+
source = next;
|
|
137
|
+
index += 1;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (token.startsWith("--source=")) {
|
|
141
|
+
source = token.slice("--source=".length);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (token === "--preset") {
|
|
145
|
+
const next = tokens[index + 1];
|
|
146
|
+
if (!next) {
|
|
147
|
+
throw new Error("Missing value for --preset");
|
|
148
|
+
}
|
|
149
|
+
preset = next;
|
|
150
|
+
index += 1;
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (token.startsWith("--preset=")) {
|
|
154
|
+
preset = token.slice("--preset=".length);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (token.startsWith("-")) {
|
|
158
|
+
throw new Error(`Unknown option: ${token}`);
|
|
159
|
+
}
|
|
160
|
+
if (!workspaceName) {
|
|
161
|
+
workspaceName = token;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
throw new Error(`Unexpected argument: ${token}`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (workspaceName) {
|
|
168
|
+
installDir = path.resolve(installDir, workspaceName);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return { installDir, source, preset, workspaceName };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function parseDoctorArgs(tokens) {
|
|
175
|
+
let installDir = process.cwd();
|
|
176
|
+
let fix = false;
|
|
177
|
+
|
|
178
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
179
|
+
const token = tokens[index];
|
|
180
|
+
if (token === "--fix") {
|
|
181
|
+
fix = true;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (token.startsWith("-")) {
|
|
185
|
+
throw new Error(`Unknown option: ${token}`);
|
|
186
|
+
}
|
|
187
|
+
installDir = path.resolve(token);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return { installDir, fix };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function parseUpdateArgs(tokens) {
|
|
194
|
+
let installDir = process.cwd();
|
|
195
|
+
let dryRun = false;
|
|
196
|
+
|
|
197
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
198
|
+
const token = tokens[index];
|
|
199
|
+
if (token === "--dry-run") {
|
|
200
|
+
dryRun = true;
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if (token.startsWith("-")) {
|
|
204
|
+
throw new Error(`Unknown option: ${token}`);
|
|
205
|
+
}
|
|
206
|
+
installDir = path.resolve(token);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return { installDir, dryRun };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function parseRemoveArgs(tokens) {
|
|
213
|
+
let installDir = process.cwd();
|
|
214
|
+
let force = false;
|
|
215
|
+
let target = "";
|
|
216
|
+
|
|
217
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
218
|
+
const token = tokens[index];
|
|
219
|
+
if (token === "--force") {
|
|
220
|
+
force = true;
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
if (token === "--dir" || token === "--out") {
|
|
224
|
+
const next = tokens[index + 1];
|
|
225
|
+
if (!next) {
|
|
226
|
+
throw new Error(`Missing value for ${token}`);
|
|
227
|
+
}
|
|
228
|
+
installDir = path.resolve(next);
|
|
229
|
+
index += 1;
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (token.startsWith("--dir=")) {
|
|
233
|
+
installDir = path.resolve(token.slice("--dir=".length));
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (token.startsWith("--out=")) {
|
|
237
|
+
installDir = path.resolve(token.slice("--out=".length));
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
if (token.startsWith("-")) {
|
|
241
|
+
throw new Error(`Unknown option: ${token}`);
|
|
242
|
+
}
|
|
243
|
+
if (!target) {
|
|
244
|
+
target = token;
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
throw new Error(`Unexpected argument: ${token}`);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return { installDir, force, target };
|
|
251
|
+
}
|
|
252
|
+
|
|
98
253
|
function ensureGitAvailable() {
|
|
99
254
|
const result = spawnSync("git", ["--version"], { encoding: "utf8" });
|
|
100
255
|
if (result.status !== 0) {
|
|
@@ -165,6 +320,14 @@ function writeManifest(manifestPath, manifest) {
|
|
|
165
320
|
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
|
|
166
321
|
}
|
|
167
322
|
|
|
323
|
+
function readJson(filePath) {
|
|
324
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function pathExists(filePath) {
|
|
328
|
+
return fs.existsSync(filePath);
|
|
329
|
+
}
|
|
330
|
+
|
|
168
331
|
function cloneLayer(target, installDir) {
|
|
169
332
|
const repoUrl = repoUrlFor(target);
|
|
170
333
|
const targetPath = targetPathFor(installDir, target);
|
|
@@ -261,51 +424,81 @@ function readInstalledPackageName(layerPath) {
|
|
|
261
424
|
return null;
|
|
262
425
|
}
|
|
263
426
|
try {
|
|
264
|
-
return
|
|
427
|
+
return readJson(packageJsonPath).name || null;
|
|
265
428
|
} catch {
|
|
266
429
|
return null;
|
|
267
430
|
}
|
|
268
431
|
}
|
|
269
432
|
|
|
270
|
-
function
|
|
271
|
-
|
|
272
|
-
|
|
433
|
+
function readInstalledVersion(layerPath) {
|
|
434
|
+
const packageJsonPath = path.join(layerPath, "package.json");
|
|
435
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
436
|
+
return null;
|
|
273
437
|
}
|
|
274
|
-
|
|
275
|
-
return
|
|
438
|
+
try {
|
|
439
|
+
return readJson(packageJsonPath).version || null;
|
|
440
|
+
} catch {
|
|
441
|
+
return null;
|
|
276
442
|
}
|
|
277
|
-
throw new Error(`Unsupported source: ${source}. Use git or npm.`);
|
|
278
443
|
}
|
|
279
444
|
|
|
280
|
-
function
|
|
281
|
-
|
|
282
|
-
if (!fs.existsSync(manifestPath)) {
|
|
283
|
-
throw new Error(`No architectonic.json found in ${installDir}`);
|
|
284
|
-
}
|
|
285
|
-
return { manifestPath, manifest: readManifest(manifestPath) };
|
|
445
|
+
function hasGitRepo(layerPath) {
|
|
446
|
+
return pathExists(path.join(layerPath, ".git"));
|
|
286
447
|
}
|
|
287
448
|
|
|
288
|
-
function
|
|
289
|
-
|
|
449
|
+
function gitResult(args, cwd) {
|
|
450
|
+
return spawnSync("git", args, {
|
|
451
|
+
cwd,
|
|
452
|
+
encoding: "utf8",
|
|
453
|
+
stdio: "pipe",
|
|
454
|
+
});
|
|
455
|
+
}
|
|
290
456
|
|
|
291
|
-
|
|
292
|
-
|
|
457
|
+
function isGitWorktreeDirty(layerPath) {
|
|
458
|
+
const result = gitResult(["status", "--porcelain"], layerPath);
|
|
459
|
+
if (result.status !== 0) {
|
|
460
|
+
return null;
|
|
293
461
|
}
|
|
462
|
+
return Boolean((result.stdout || "").trim());
|
|
463
|
+
}
|
|
294
464
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
465
|
+
function packageMetaFor(target, layerPath) {
|
|
466
|
+
return {
|
|
467
|
+
expectedPackageName: packageMap[target] || null,
|
|
468
|
+
packageName: readInstalledPackageName(layerPath),
|
|
469
|
+
version: readInstalledVersion(layerPath),
|
|
470
|
+
};
|
|
471
|
+
}
|
|
299
472
|
|
|
473
|
+
function relativeManifestPath(installDir, absolutePath) {
|
|
474
|
+
return `./${path.relative(installDir, absolutePath).replace(/\\/g, "/")}`;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function ensureInstallPrereqs(source) {
|
|
300
478
|
if (source === "git") {
|
|
301
479
|
ensureGitAvailable();
|
|
302
|
-
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
if (source === "npm") {
|
|
303
483
|
ensureNpmAvailable();
|
|
304
484
|
ensureTarAvailable();
|
|
305
|
-
|
|
306
|
-
throw new Error(`Unsupported source: ${source}. Use git or npm.`);
|
|
485
|
+
return;
|
|
307
486
|
}
|
|
487
|
+
throw new Error(`Unsupported source: ${source}. Use git or npm.`);
|
|
488
|
+
}
|
|
308
489
|
|
|
490
|
+
function updateManifestLayerRecord(manifest, installDir, layerName, absoluteLayerPath, source, refOverride = null) {
|
|
491
|
+
manifest.layers[layerName] = {
|
|
492
|
+
source,
|
|
493
|
+
ref: refOverride ?? manifest.layers[layerName]?.ref ?? (source === "git" ? repoUrlFor(layerName) : packageSpecFor(layerName)),
|
|
494
|
+
path: relativeManifestPath(installDir, absoluteLayerPath),
|
|
495
|
+
installed_at: new Date().toISOString(),
|
|
496
|
+
package_name: readInstalledPackageName(absoluteLayerPath),
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function installTargets(targets, installDir, source) {
|
|
501
|
+
ensureInstallPrereqs(source);
|
|
309
502
|
fs.mkdirSync(installDir, { recursive: true });
|
|
310
503
|
|
|
311
504
|
const installed = [];
|
|
@@ -322,13 +515,7 @@ function addCommand(tokens) {
|
|
|
322
515
|
manifest.npm_source_base = npmBase || "registry";
|
|
323
516
|
|
|
324
517
|
for (const item of installed) {
|
|
325
|
-
manifest
|
|
326
|
-
source: item.source,
|
|
327
|
-
ref: item.repo,
|
|
328
|
-
path: `./${path.relative(installDir, item.path).replace(/\\/g, "/")}`,
|
|
329
|
-
installed_at: manifest.installed_at,
|
|
330
|
-
package_name: readInstalledPackageName(item.path),
|
|
331
|
-
};
|
|
518
|
+
updateManifestLayerRecord(manifest, installDir, item.name, item.path, item.source, item.repo);
|
|
332
519
|
}
|
|
333
520
|
|
|
334
521
|
writeManifest(manifestPath, manifest);
|
|
@@ -338,6 +525,86 @@ function addCommand(tokens) {
|
|
|
338
525
|
console.log(`Wrote ${manifestPath}`);
|
|
339
526
|
}
|
|
340
527
|
|
|
528
|
+
function installLayer(target, installDir, source) {
|
|
529
|
+
if (source === "git") {
|
|
530
|
+
return cloneLayer(target, installDir);
|
|
531
|
+
}
|
|
532
|
+
if (source === "npm") {
|
|
533
|
+
return packNpmLayer(target, installDir);
|
|
534
|
+
}
|
|
535
|
+
throw new Error(`Unsupported source: ${source}. Use git or npm.`);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function loadManifestFromDir(installDir) {
|
|
539
|
+
const manifestPath = manifestPathFor(installDir);
|
|
540
|
+
if (!fs.existsSync(manifestPath)) {
|
|
541
|
+
throw new Error(`No architectonic.json found in ${installDir}`);
|
|
542
|
+
}
|
|
543
|
+
return { manifestPath, manifest: readManifest(manifestPath) };
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function addCommand(tokens) {
|
|
547
|
+
const { targets, installDir, source } = parseAddArgs(tokens);
|
|
548
|
+
|
|
549
|
+
if (!targets.length) {
|
|
550
|
+
throw new Error("Specify one or more layers: teleology, identity, project, skills");
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
const invalid = targets.filter((target) => !supportedSet.has(target));
|
|
554
|
+
if (invalid.length) {
|
|
555
|
+
throw new Error(`Unknown layer(s): ${invalid.join(", ")}\nSupported layers: ${supported.join(", ")}`);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
installTargets(targets, installDir, source);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function resolvePreset(preset) {
|
|
562
|
+
const presets = {
|
|
563
|
+
solo: ["teleology", "identity", "project", "skills"],
|
|
564
|
+
company: ["teleology", "project", "skills"],
|
|
565
|
+
project: ["project", "skills"],
|
|
566
|
+
agent: ["identity", "skills"],
|
|
567
|
+
};
|
|
568
|
+
const layers = presets[preset];
|
|
569
|
+
if (!layers) {
|
|
570
|
+
throw new Error(`Unknown preset: ${preset}. Supported presets: ${Object.keys(presets).join(", ")}`);
|
|
571
|
+
}
|
|
572
|
+
return layers;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function writeInitFiles(installDir, workspaceName, layers) {
|
|
576
|
+
const displayName = workspaceName || path.basename(installDir);
|
|
577
|
+
const readmePath = path.join(installDir, "README.md");
|
|
578
|
+
const agentsPath = path.join(installDir, "AGENTS.md");
|
|
579
|
+
|
|
580
|
+
if (!pathExists(readmePath)) {
|
|
581
|
+
fs.writeFileSync(
|
|
582
|
+
readmePath,
|
|
583
|
+
`# ${displayName}\n\nThis workspace was initialized by \`architectonic\`.\n\nInstalled layers:\n\n${layers.map((layer) => `- ${layer}`).join("\n")}\n`,
|
|
584
|
+
"utf8",
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
if (!pathExists(agentsPath)) {
|
|
589
|
+
fs.writeFileSync(
|
|
590
|
+
agentsPath,
|
|
591
|
+
`# Agent Instructions\n\nRead installed layers before making structural changes.\n\nPriority order:\n1. teleology\n2. identity\n3. project\n4. skills\n`,
|
|
592
|
+
"utf8",
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function initCommand(tokens) {
|
|
598
|
+
const { installDir, source, preset, workspaceName } = parseInitArgs(tokens);
|
|
599
|
+
if (pathExists(installDir) && fs.readdirSync(installDir).length > 0) {
|
|
600
|
+
throw new Error(`Refusing to initialize into a non-empty directory: ${installDir}`);
|
|
601
|
+
}
|
|
602
|
+
fs.mkdirSync(installDir, { recursive: true });
|
|
603
|
+
const layers = resolvePreset(preset);
|
|
604
|
+
writeInitFiles(installDir, workspaceName, layers);
|
|
605
|
+
installTargets(layers, installDir, source);
|
|
606
|
+
}
|
|
607
|
+
|
|
341
608
|
function listCommand(tokens) {
|
|
342
609
|
const installDir = tokens[0] ? path.resolve(tokens[0]) : process.cwd();
|
|
343
610
|
const { manifest } = loadManifestFromDir(installDir);
|
|
@@ -359,11 +626,17 @@ function listCommand(tokens) {
|
|
|
359
626
|
}
|
|
360
627
|
}
|
|
361
628
|
|
|
629
|
+
function inferLayerPath(installDir, layerName) {
|
|
630
|
+
const candidate = targetPathFor(installDir, layerName);
|
|
631
|
+
return pathExists(candidate) ? candidate : null;
|
|
632
|
+
}
|
|
633
|
+
|
|
362
634
|
function doctorCommand(tokens) {
|
|
363
|
-
const installDir
|
|
635
|
+
const { installDir, fix } = parseDoctorArgs(tokens);
|
|
364
636
|
const { manifestPath, manifest } = loadManifestFromDir(installDir);
|
|
365
637
|
const entries = Object.entries(manifest.layers || {});
|
|
366
638
|
let failures = 0;
|
|
639
|
+
let changed = false;
|
|
367
640
|
|
|
368
641
|
console.log(`architectonic doctor`);
|
|
369
642
|
console.log(` root: ${installDir}`);
|
|
@@ -384,6 +657,13 @@ function doctorCommand(tokens) {
|
|
|
384
657
|
const expectedPackageName = packageMap[name];
|
|
385
658
|
|
|
386
659
|
if (!exists) {
|
|
660
|
+
const inferredPath = inferLayerPath(installDir, name);
|
|
661
|
+
if (fix && inferredPath) {
|
|
662
|
+
updateManifestLayerRecord(manifest, installDir, name, inferredPath, layer.source || "unknown", layer.ref || null);
|
|
663
|
+
changed = true;
|
|
664
|
+
console.log(` [fix] ${name}: repointed path to ${relativeManifestPath(installDir, inferredPath)}`);
|
|
665
|
+
continue;
|
|
666
|
+
}
|
|
387
667
|
console.log(` [fail] ${name}: missing directory ${layerPath}`);
|
|
388
668
|
failures += 1;
|
|
389
669
|
continue;
|
|
@@ -396,25 +676,178 @@ function doctorCommand(tokens) {
|
|
|
396
676
|
}
|
|
397
677
|
|
|
398
678
|
if (packageName && expectedPackageName && packageName !== expectedPackageName) {
|
|
679
|
+
if (fix) {
|
|
680
|
+
manifest.layers[name].package_name = packageName;
|
|
681
|
+
manifest.layers[name].path = relativeManifestPath(installDir, layerPath);
|
|
682
|
+
changed = true;
|
|
683
|
+
console.log(` [fix] ${name}: synced manifest package name to ${packageName}`);
|
|
684
|
+
continue;
|
|
685
|
+
}
|
|
399
686
|
console.log(` [fail] ${name}: expected package ${expectedPackageName}, found ${packageName}`);
|
|
400
687
|
failures += 1;
|
|
401
688
|
continue;
|
|
402
689
|
}
|
|
403
690
|
|
|
691
|
+
if (fix) {
|
|
692
|
+
const normalizedPath = relativeManifestPath(installDir, layerPath);
|
|
693
|
+
if (manifest.layers[name].path !== normalizedPath || manifest.layers[name].package_name !== packageName) {
|
|
694
|
+
manifest.layers[name].path = normalizedPath;
|
|
695
|
+
manifest.layers[name].package_name = packageName;
|
|
696
|
+
changed = true;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
404
700
|
console.log(` [ok] ${name}: ${relativePath} (${layer.source || "unknown"})`);
|
|
405
701
|
}
|
|
406
702
|
|
|
703
|
+
if (fix && changed) {
|
|
704
|
+
writeManifest(manifestPath, manifest);
|
|
705
|
+
console.log(` [write] updated manifest repairs`);
|
|
706
|
+
}
|
|
707
|
+
|
|
407
708
|
if (failures > 0) {
|
|
408
709
|
process.exit(1);
|
|
409
710
|
}
|
|
410
711
|
}
|
|
411
712
|
|
|
713
|
+
function updateGitLayer(layerName, layerPath, dryRun) {
|
|
714
|
+
if (!hasGitRepo(layerPath)) {
|
|
715
|
+
return { status: "skip", detail: "not a git repository" };
|
|
716
|
+
}
|
|
717
|
+
const dirty = isGitWorktreeDirty(layerPath);
|
|
718
|
+
if (dirty === null) {
|
|
719
|
+
return { status: "skip", detail: "unable to inspect git status" };
|
|
720
|
+
}
|
|
721
|
+
if (dirty) {
|
|
722
|
+
return { status: "skip", detail: "local changes detected; preserving fork" };
|
|
723
|
+
}
|
|
724
|
+
if (dryRun) {
|
|
725
|
+
return { status: "plan", detail: "would run git pull --ff-only" };
|
|
726
|
+
}
|
|
727
|
+
const result = gitResult(["pull", "--ff-only"], layerPath);
|
|
728
|
+
if (result.status !== 0) {
|
|
729
|
+
return { status: "fail", detail: (result.stderr || result.stdout || "").trim() || "git pull failed" };
|
|
730
|
+
}
|
|
731
|
+
return { status: "ok", detail: (result.stdout || "").trim() || "up to date" };
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function getLatestNpmVersion(packageSpec) {
|
|
735
|
+
const result = spawnSync("npm", ["view", packageSpec, "version"], {
|
|
736
|
+
encoding: "utf8",
|
|
737
|
+
stdio: "pipe",
|
|
738
|
+
shell: true,
|
|
739
|
+
});
|
|
740
|
+
if (result.status !== 0) {
|
|
741
|
+
return null;
|
|
742
|
+
}
|
|
743
|
+
return (result.stdout || "").trim() || null;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function updateNpmLayer(layerName, layerPath) {
|
|
747
|
+
const installedVersion = readInstalledVersion(layerPath);
|
|
748
|
+
const packageSpec = packageSpecFor(layerName);
|
|
749
|
+
const latestVersion = getLatestNpmVersion(packageSpec);
|
|
750
|
+
if (!latestVersion) {
|
|
751
|
+
return { status: "skip", detail: "unable to resolve latest npm version" };
|
|
752
|
+
}
|
|
753
|
+
if (!installedVersion) {
|
|
754
|
+
return { status: "skip", detail: `latest ${latestVersion} available; local version unknown` };
|
|
755
|
+
}
|
|
756
|
+
if (installedVersion === latestVersion) {
|
|
757
|
+
return { status: "ok", detail: `already at ${installedVersion}` };
|
|
758
|
+
}
|
|
759
|
+
return {
|
|
760
|
+
status: "skip",
|
|
761
|
+
detail: `newer package ${latestVersion} available; skipped to preserve local fork (${installedVersion} installed)`,
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
function updateCommand(tokens) {
|
|
766
|
+
const { installDir, dryRun } = parseUpdateArgs(tokens);
|
|
767
|
+
const { manifestPath, manifest } = loadManifestFromDir(installDir);
|
|
768
|
+
const entries = Object.entries(manifest.layers || {});
|
|
769
|
+
|
|
770
|
+
if (!entries.length) {
|
|
771
|
+
throw new Error("No installed layers recorded.");
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
ensureGitAvailable();
|
|
775
|
+
ensureNpmAvailable();
|
|
776
|
+
|
|
777
|
+
let changed = false;
|
|
778
|
+
console.log(`architectonic update${dryRun ? " --dry-run" : ""}`);
|
|
779
|
+
console.log(` root: ${installDir}`);
|
|
780
|
+
|
|
781
|
+
for (const [name, layer] of entries) {
|
|
782
|
+
const layerPath = path.resolve(installDir, String(layer.path || ""));
|
|
783
|
+
if (!pathExists(layerPath)) {
|
|
784
|
+
console.log(` [skip] ${name}: missing directory`);
|
|
785
|
+
continue;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
let outcome;
|
|
789
|
+
if (layer.source === "git") {
|
|
790
|
+
outcome = updateGitLayer(name, layerPath, dryRun);
|
|
791
|
+
} else if (layer.source === "npm") {
|
|
792
|
+
outcome = updateNpmLayer(name, layerPath);
|
|
793
|
+
} else {
|
|
794
|
+
outcome = { status: "skip", detail: `unsupported source ${layer.source || "unknown"}` };
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
console.log(` [${outcome.status}] ${name}: ${outcome.detail}`);
|
|
798
|
+
if (outcome.status === "ok" && !dryRun) {
|
|
799
|
+
manifest.layers[name].installed_at = new Date().toISOString();
|
|
800
|
+
manifest.layers[name].package_name = readInstalledPackageName(layerPath);
|
|
801
|
+
changed = true;
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
if (changed) {
|
|
806
|
+
writeManifest(manifestPath, manifest);
|
|
807
|
+
console.log(` [write] refreshed manifest timestamps`);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
function removeCommand(tokens) {
|
|
812
|
+
const { installDir, force, target } = parseRemoveArgs(tokens);
|
|
813
|
+
if (!target) {
|
|
814
|
+
throw new Error("Specify a layer to remove.");
|
|
815
|
+
}
|
|
816
|
+
if (!supportedSet.has(target)) {
|
|
817
|
+
throw new Error(`Unknown layer: ${target}`);
|
|
818
|
+
}
|
|
819
|
+
const { manifestPath, manifest } = loadManifestFromDir(installDir);
|
|
820
|
+
const layer = manifest.layers[target];
|
|
821
|
+
if (!layer) {
|
|
822
|
+
throw new Error(`Layer not recorded in manifest: ${target}`);
|
|
823
|
+
}
|
|
824
|
+
const layerPath = path.resolve(installDir, String(layer.path || ""));
|
|
825
|
+
if (pathExists(layerPath) && hasGitRepo(layerPath) && !force) {
|
|
826
|
+
const dirty = isGitWorktreeDirty(layerPath);
|
|
827
|
+
if (dirty) {
|
|
828
|
+
throw new Error(`Refusing to remove ${target}: local git changes detected. Re-run with --force if you really want to delete it.`);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
if (pathExists(layerPath)) {
|
|
832
|
+
fs.rmSync(layerPath, { recursive: true, force: true });
|
|
833
|
+
}
|
|
834
|
+
delete manifest.layers[target];
|
|
835
|
+
writeManifest(manifestPath, manifest);
|
|
836
|
+
console.log(`Removed ${target}`);
|
|
837
|
+
console.log(`Updated ${manifestPath}`);
|
|
838
|
+
}
|
|
839
|
+
|
|
412
840
|
try {
|
|
413
841
|
if (!command || command === "help" || command === "--help" || command === "-h") {
|
|
414
842
|
printHelp();
|
|
415
843
|
process.exit(0);
|
|
416
844
|
}
|
|
417
845
|
|
|
846
|
+
if (command === "init") {
|
|
847
|
+
initCommand(rest);
|
|
848
|
+
process.exit(0);
|
|
849
|
+
}
|
|
850
|
+
|
|
418
851
|
if (command === "add") {
|
|
419
852
|
addCommand(rest);
|
|
420
853
|
process.exit(0);
|
|
@@ -430,7 +863,17 @@ try {
|
|
|
430
863
|
process.exit(0);
|
|
431
864
|
}
|
|
432
865
|
|
|
433
|
-
|
|
866
|
+
if (command === "update") {
|
|
867
|
+
updateCommand(rest);
|
|
868
|
+
process.exit(0);
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
if (command === "remove") {
|
|
872
|
+
removeCommand(rest);
|
|
873
|
+
process.exit(0);
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
printUsageError(`Unknown command: ${command}`);
|
|
434
877
|
} catch (error) {
|
|
435
878
|
console.error(error.message);
|
|
436
879
|
process.exit(1);
|