architectonic 0.0.3 → 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 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.3`, `architectonic add` becomes real: it clones the selected layer
16
- repositories into a target directory and records them in `architectonic.json`.
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`,
@@ -44,9 +47,18 @@ npx architectonic add project
44
47
  npx architectonic add skills
45
48
  npx architectonic add teleology identity skills
46
49
  npx architectonic add skills --dir ./vendor
50
+ npx architectonic add teleology --source npm
51
+ npx architectonic init MyWorkspace
52
+ npx architectonic init --preset company
53
+ npx architectonic list
54
+ npx architectonic doctor
55
+ npx architectonic doctor --fix
56
+ npx architectonic update
57
+ npx architectonic update --dry-run
58
+ npx architectonic remove skills
47
59
  ```
48
60
 
49
- `add` clones from the Architectonic GitHub organization into the current
61
+ `add` installs from the Architectonic GitHub organization into the current
50
62
  directory by default:
51
63
 
52
64
  ```text
@@ -62,6 +74,56 @@ directory by default:
62
74
  If a target directory already exists, the command stops instead of silently
63
75
  overwriting it.
64
76
 
77
+ ## Sources
78
+
79
+ `add` supports two source modes:
80
+
81
+ ```text
82
+ --source git # clone from GitHub or another git base
83
+ --source npm # pack and extract from npm packages
84
+ ```
85
+
86
+ The default is `git`.
87
+
88
+ Environment overrides:
89
+
90
+ ```text
91
+ ARCHITECTONIC_SOURCE_BASE # override the git source base
92
+ ARCHITECTONIC_NPM_BASE # override the npm package base or local package root
93
+ ARCHITECTONIC_ADD_SOURCE # change the default source mode
94
+ ```
95
+
96
+ `list` reads `architectonic.json` and shows installed layers.
97
+
98
+ `doctor` verifies that each recorded layer still exists and that the installed
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.
126
+
65
127
  ## Run vs install
66
128
 
67
129
  ```text
@@ -4,12 +4,19 @@ 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.3";
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"];
11
11
  const supportedSet = new Set(supported);
12
12
  const repoBase = process.env.ARCHITECTONIC_SOURCE_BASE || "https://github.com/architectonic";
13
+ const npmBase = process.env.ARCHITECTONIC_NPM_BASE || "";
14
+ const packageMap = {
15
+ teleology: "teleology",
16
+ identity: "architectonic-identity",
17
+ project: "architectonic-project",
18
+ skills: "architectonic-skills",
19
+ };
13
20
 
14
21
  function printHelp() {
15
22
  console.log(`architectonic ${VERSION}
@@ -22,6 +29,13 @@ Usage:
22
29
  npx architectonic add <teleology|identity|project|skills>
23
30
  npx architectonic add teleology identity skills
24
31
  npx architectonic add skills --dir ./vendor
32
+ npx architectonic add teleology --source npm
33
+ npx architectonic init [name]
34
+ npx architectonic list
35
+ npx architectonic doctor
36
+ npx architectonic doctor --fix
37
+ npx architectonic update
38
+ npx architectonic remove <layer>
25
39
 
26
40
  Layers:
27
41
  teleology purpose, principles, doctrine, governance
@@ -35,13 +49,18 @@ Run vs install:
35
49
  npm install -g architectonic install globally
36
50
 
37
51
  What add does:
38
- Clones the selected layer repositories into the target directory
39
- and records them in architectonic.json.`);
52
+ Installs the selected layer repositories into the target directory
53
+ from git or npm sources and records them in architectonic.json.`);
54
+ }
55
+
56
+ function printUsageError(message) {
57
+ throw new Error(`${message}\nRun \`architectonic help\` for usage.`);
40
58
  }
41
59
 
42
60
  function parseAddArgs(tokens) {
43
61
  const targets = [];
44
62
  let installDir = process.cwd();
63
+ let source = process.env.ARCHITECTONIC_ADD_SOURCE || "git";
45
64
 
46
65
  for (let index = 0; index < tokens.length; index += 1) {
47
66
  const token = tokens[index];
@@ -62,13 +81,173 @@ function parseAddArgs(tokens) {
62
81
  installDir = path.resolve(token.slice("--out=".length));
63
82
  continue;
64
83
  }
84
+ if (token === "--source") {
85
+ const next = tokens[index + 1];
86
+ if (!next) {
87
+ throw new Error("Missing value for --source");
88
+ }
89
+ source = next;
90
+ index += 1;
91
+ continue;
92
+ }
93
+ if (token.startsWith("--source=")) {
94
+ source = token.slice("--source=".length);
95
+ continue;
96
+ }
65
97
  if (token.startsWith("-")) {
66
98
  throw new Error(`Unknown option: ${token}`);
67
99
  }
68
100
  targets.push(token);
69
101
  }
70
102
 
71
- return { targets, installDir };
103
+ return { targets, installDir, source };
104
+ }
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 };
72
251
  }
73
252
 
74
253
  function ensureGitAvailable() {
@@ -78,6 +257,20 @@ function ensureGitAvailable() {
78
257
  }
79
258
  }
80
259
 
260
+ function ensureNpmAvailable() {
261
+ const result = spawnSync("npm", ["--version"], { encoding: "utf8", shell: true });
262
+ if (result.status !== 0) {
263
+ throw new Error("npm is required on PATH for `architectonic add --source npm`.");
264
+ }
265
+ }
266
+
267
+ function ensureTarAvailable() {
268
+ const result = spawnSync("tar", ["--version"], { encoding: "utf8" });
269
+ if (result.status !== 0) {
270
+ throw new Error("tar is required on PATH for npm package extraction.");
271
+ }
272
+ }
273
+
81
274
  function repoUrlFor(target) {
82
275
  const normalizedBase = repoBase.replace(/\\/g, "/");
83
276
  if (/^(?:[A-Za-z]:\/|\/|\.{1,2}\/)/.test(normalizedBase)) {
@@ -86,10 +279,31 @@ function repoUrlFor(target) {
86
279
  return `${normalizedBase}/${target}.git`;
87
280
  }
88
281
 
282
+ function packageSpecFor(target) {
283
+ const packageName = packageMap[target];
284
+ if (!packageName) {
285
+ throw new Error(`No npm package mapping defined for ${target}`);
286
+ }
287
+ if (!npmBase) {
288
+ return packageName;
289
+ }
290
+ const normalizedBase = npmBase.replace(/\\/g, "/");
291
+ if (/^(?:[A-Za-z]:\/|\/|\.{1,2}\/)/.test(normalizedBase)) {
292
+ return path.resolve(normalizedBase, target);
293
+ }
294
+ return normalizedBase.endsWith("/")
295
+ ? `${normalizedBase}${packageName}`
296
+ : `${normalizedBase}/${packageName}`;
297
+ }
298
+
89
299
  function targetPathFor(installDir, target) {
90
300
  return path.join(installDir, target);
91
301
  }
92
302
 
303
+ function manifestPathFor(installDir) {
304
+ return path.join(installDir, "architectonic.json");
305
+ }
306
+
93
307
  function readManifest(manifestPath) {
94
308
  if (!fs.existsSync(manifestPath)) {
95
309
  return {
@@ -106,6 +320,14 @@ function writeManifest(manifestPath, manifest) {
106
320
  fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
107
321
  }
108
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
+
109
331
  function cloneLayer(target, installDir) {
110
332
  const repoUrl = repoUrlFor(target);
111
333
  const targetPath = targetPathFor(installDir, target);
@@ -129,41 +351,171 @@ function cloneLayer(target, installDir) {
129
351
  name: target,
130
352
  repo: repoUrl,
131
353
  path: targetPath,
354
+ source: "git",
132
355
  };
133
356
  }
134
357
 
135
- function addCommand(tokens) {
136
- const { targets, installDir } = parseAddArgs(tokens);
358
+ function packNpmLayer(target, installDir) {
359
+ const packageSpec = packageSpecFor(target);
360
+ const targetPath = targetPathFor(installDir, target);
361
+ const tempRoot = fs.mkdtempSync(path.join(installDir, "architectonic-npm-"));
362
+ const pack = spawnSync("npm", ["pack", packageSpec, "--pack-destination", tempRoot], {
363
+ cwd: installDir,
364
+ encoding: "utf8",
365
+ stdio: "pipe",
366
+ shell: true,
367
+ });
137
368
 
138
- if (!targets.length) {
139
- throw new Error("Specify one or more layers: teleology, identity, project, skills");
369
+ if (pack.status !== 0) {
370
+ const detail = (pack.stderr || pack.stdout || "").trim();
371
+ throw new Error(`Failed to pack ${packageSpec}${detail ? `\n${detail}` : ""}`);
140
372
  }
141
373
 
142
- const invalid = targets.filter((target) => !supportedSet.has(target));
143
- if (invalid.length) {
144
- throw new Error(`Unknown layer(s): ${invalid.join(", ")}\nSupported layers: ${supported.join(", ")}`);
374
+ const tgzName = (pack.stdout || "").trim().split(/\r?\n/).filter(Boolean).pop();
375
+ if (!tgzName) {
376
+ throw new Error(`npm pack did not return a tarball name for ${packageSpec}`);
145
377
  }
146
378
 
147
- ensureGitAvailable();
379
+ const tgzPath = path.join(tempRoot, tgzName);
380
+ if (!fs.existsSync(tgzPath)) {
381
+ throw new Error(`Packed tarball not found: ${tgzPath}`);
382
+ }
383
+
384
+ if (fs.existsSync(targetPath)) {
385
+ throw new Error(`Target already exists: ${targetPath}`);
386
+ }
387
+
388
+ fs.mkdirSync(targetPath, { recursive: true });
389
+ const extract = spawnSync("tar", ["-xzf", tgzPath, "-C", targetPath], {
390
+ cwd: installDir,
391
+ encoding: "utf8",
392
+ stdio: "pipe",
393
+ });
394
+
395
+ if (extract.status !== 0) {
396
+ const detail = (extract.stderr || extract.stdout || "").trim();
397
+ throw new Error(`Failed to extract ${tgzPath}${detail ? `\n${detail}` : ""}`);
398
+ }
399
+
400
+ const extractedPackageDir = path.join(targetPath, "package");
401
+ if (!fs.existsSync(extractedPackageDir)) {
402
+ throw new Error(`Expected extracted package directory at ${extractedPackageDir}`);
403
+ }
404
+
405
+ for (const entry of fs.readdirSync(extractedPackageDir, { withFileTypes: true })) {
406
+ const from = path.join(extractedPackageDir, entry.name);
407
+ const to = path.join(targetPath, entry.name);
408
+ fs.renameSync(from, to);
409
+ }
410
+ fs.rmdirSync(extractedPackageDir);
411
+ fs.rmSync(tempRoot, { recursive: true, force: true });
412
+
413
+ return {
414
+ name: target,
415
+ repo: packageSpec,
416
+ path: targetPath,
417
+ source: "npm",
418
+ };
419
+ }
420
+
421
+ function readInstalledPackageName(layerPath) {
422
+ const packageJsonPath = path.join(layerPath, "package.json");
423
+ if (!fs.existsSync(packageJsonPath)) {
424
+ return null;
425
+ }
426
+ try {
427
+ return readJson(packageJsonPath).name || null;
428
+ } catch {
429
+ return null;
430
+ }
431
+ }
432
+
433
+ function readInstalledVersion(layerPath) {
434
+ const packageJsonPath = path.join(layerPath, "package.json");
435
+ if (!fs.existsSync(packageJsonPath)) {
436
+ return null;
437
+ }
438
+ try {
439
+ return readJson(packageJsonPath).version || null;
440
+ } catch {
441
+ return null;
442
+ }
443
+ }
444
+
445
+ function hasGitRepo(layerPath) {
446
+ return pathExists(path.join(layerPath, ".git"));
447
+ }
448
+
449
+ function gitResult(args, cwd) {
450
+ return spawnSync("git", args, {
451
+ cwd,
452
+ encoding: "utf8",
453
+ stdio: "pipe",
454
+ });
455
+ }
456
+
457
+ function isGitWorktreeDirty(layerPath) {
458
+ const result = gitResult(["status", "--porcelain"], layerPath);
459
+ if (result.status !== 0) {
460
+ return null;
461
+ }
462
+ return Boolean((result.stdout || "").trim());
463
+ }
464
+
465
+ function packageMetaFor(target, layerPath) {
466
+ return {
467
+ expectedPackageName: packageMap[target] || null,
468
+ packageName: readInstalledPackageName(layerPath),
469
+ version: readInstalledVersion(layerPath),
470
+ };
471
+ }
472
+
473
+ function relativeManifestPath(installDir, absolutePath) {
474
+ return `./${path.relative(installDir, absolutePath).replace(/\\/g, "/")}`;
475
+ }
476
+
477
+ function ensureInstallPrereqs(source) {
478
+ if (source === "git") {
479
+ ensureGitAvailable();
480
+ return;
481
+ }
482
+ if (source === "npm") {
483
+ ensureNpmAvailable();
484
+ ensureTarAvailable();
485
+ return;
486
+ }
487
+ throw new Error(`Unsupported source: ${source}. Use git or npm.`);
488
+ }
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);
148
502
  fs.mkdirSync(installDir, { recursive: true });
149
503
 
150
504
  const installed = [];
151
505
  for (const target of targets) {
152
- console.log(`Adding ${target}...`);
153
- installed.push(cloneLayer(target, installDir));
506
+ console.log(`Adding ${target} from ${source}...`);
507
+ installed.push(installLayer(target, installDir, source));
154
508
  }
155
509
 
156
- const manifestPath = path.join(installDir, "architectonic.json");
510
+ const manifestPath = manifestPathFor(installDir);
157
511
  const manifest = readManifest(manifestPath);
158
512
  manifest.installed_at = new Date().toISOString();
159
- manifest.source_base = repoBase;
513
+ manifest.last_source = source;
514
+ manifest.git_source_base = repoBase;
515
+ manifest.npm_source_base = npmBase || "registry";
160
516
 
161
517
  for (const item of installed) {
162
- manifest.layers[item.name] = {
163
- repo: item.repo,
164
- path: `./${path.relative(installDir, item.path).replace(/\\/g, "/")}`,
165
- installed_at: manifest.installed_at,
166
- };
518
+ updateManifestLayerRecord(manifest, installDir, item.name, item.path, item.source, item.repo);
167
519
  }
168
520
 
169
521
  writeManifest(manifestPath, manifest);
@@ -173,18 +525,355 @@ function addCommand(tokens) {
173
525
  console.log(`Wrote ${manifestPath}`);
174
526
  }
175
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
+
608
+ function listCommand(tokens) {
609
+ const installDir = tokens[0] ? path.resolve(tokens[0]) : process.cwd();
610
+ const { manifest } = loadManifestFromDir(installDir);
611
+ const entries = Object.entries(manifest.layers || {});
612
+
613
+ if (!entries.length) {
614
+ console.log("No layers installed.");
615
+ return;
616
+ }
617
+
618
+ for (const [name, layer] of entries) {
619
+ console.log(`${name}`);
620
+ console.log(` source: ${layer.source || "unknown"}`);
621
+ console.log(` path: ${layer.path || "unknown"}`);
622
+ console.log(` ref: ${layer.ref || "unknown"}`);
623
+ if (layer.package_name) {
624
+ console.log(` pkg: ${layer.package_name}`);
625
+ }
626
+ }
627
+ }
628
+
629
+ function inferLayerPath(installDir, layerName) {
630
+ const candidate = targetPathFor(installDir, layerName);
631
+ return pathExists(candidate) ? candidate : null;
632
+ }
633
+
634
+ function doctorCommand(tokens) {
635
+ const { installDir, fix } = parseDoctorArgs(tokens);
636
+ const { manifestPath, manifest } = loadManifestFromDir(installDir);
637
+ const entries = Object.entries(manifest.layers || {});
638
+ let failures = 0;
639
+ let changed = false;
640
+
641
+ console.log(`architectonic doctor`);
642
+ console.log(` root: ${installDir}`);
643
+ console.log(` manifest: ${manifestPath}`);
644
+
645
+ if (!entries.length) {
646
+ console.log(` [fail] no installed layers recorded`);
647
+ process.exit(1);
648
+ }
649
+
650
+ for (const [name, layer] of entries) {
651
+ const relativePath = String(layer.path || "");
652
+ const layerPath = path.resolve(installDir, relativePath);
653
+ const packageJsonPath = path.join(layerPath, "package.json");
654
+ const exists = fs.existsSync(layerPath);
655
+ const hasPackageJson = fs.existsSync(packageJsonPath);
656
+ const packageName = hasPackageJson ? readInstalledPackageName(layerPath) : null;
657
+ const expectedPackageName = packageMap[name];
658
+
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
+ }
667
+ console.log(` [fail] ${name}: missing directory ${layerPath}`);
668
+ failures += 1;
669
+ continue;
670
+ }
671
+
672
+ if (!hasPackageJson) {
673
+ console.log(` [fail] ${name}: missing package.json`);
674
+ failures += 1;
675
+ continue;
676
+ }
677
+
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
+ }
686
+ console.log(` [fail] ${name}: expected package ${expectedPackageName}, found ${packageName}`);
687
+ failures += 1;
688
+ continue;
689
+ }
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
+
700
+ console.log(` [ok] ${name}: ${relativePath} (${layer.source || "unknown"})`);
701
+ }
702
+
703
+ if (fix && changed) {
704
+ writeManifest(manifestPath, manifest);
705
+ console.log(` [write] updated manifest repairs`);
706
+ }
707
+
708
+ if (failures > 0) {
709
+ process.exit(1);
710
+ }
711
+ }
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
+
176
840
  try {
177
841
  if (!command || command === "help" || command === "--help" || command === "-h") {
178
842
  printHelp();
179
843
  process.exit(0);
180
844
  }
181
845
 
846
+ if (command === "init") {
847
+ initCommand(rest);
848
+ process.exit(0);
849
+ }
850
+
182
851
  if (command === "add") {
183
852
  addCommand(rest);
184
853
  process.exit(0);
185
854
  }
186
855
 
187
- throw new Error(`Unknown command: ${command}\nRun \`architectonic help\` for usage.`);
856
+ if (command === "list") {
857
+ listCommand(rest);
858
+ process.exit(0);
859
+ }
860
+
861
+ if (command === "doctor") {
862
+ doctorCommand(rest);
863
+ process.exit(0);
864
+ }
865
+
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}`);
188
877
  } catch (error) {
189
878
  console.error(error.message);
190
879
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "architectonic",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "description": "CLI for composing agentic system layers such as teleology, identity, project, and skills.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",