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 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.4`, `architectonic add` installs the selected layer repositories from
16
- either git or npm sources 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`,
@@ -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
 
@@ -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.4";
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 JSON.parse(fs.readFileSync(packageJsonPath, "utf8")).name || null;
427
+ return readJson(packageJsonPath).name || null;
265
428
  } catch {
266
429
  return null;
267
430
  }
268
431
  }
269
432
 
270
- function installLayer(target, installDir, source) {
271
- if (source === "git") {
272
- return cloneLayer(target, installDir);
433
+ function readInstalledVersion(layerPath) {
434
+ const packageJsonPath = path.join(layerPath, "package.json");
435
+ if (!fs.existsSync(packageJsonPath)) {
436
+ return null;
273
437
  }
274
- if (source === "npm") {
275
- return packNpmLayer(target, installDir);
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 loadManifestFromDir(installDir) {
281
- const manifestPath = manifestPathFor(installDir);
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 addCommand(tokens) {
289
- const { targets, installDir, source } = parseAddArgs(tokens);
449
+ function gitResult(args, cwd) {
450
+ return spawnSync("git", args, {
451
+ cwd,
452
+ encoding: "utf8",
453
+ stdio: "pipe",
454
+ });
455
+ }
290
456
 
291
- if (!targets.length) {
292
- throw new Error("Specify one or more layers: teleology, identity, project, skills");
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
- const invalid = targets.filter((target) => !supportedSet.has(target));
296
- if (invalid.length) {
297
- throw new Error(`Unknown layer(s): ${invalid.join(", ")}\nSupported layers: ${supported.join(", ")}`);
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
- } else if (source === "npm") {
480
+ return;
481
+ }
482
+ if (source === "npm") {
303
483
  ensureNpmAvailable();
304
484
  ensureTarAvailable();
305
- } else {
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.layers[item.name] = {
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 = tokens[0] ? path.resolve(tokens[0]) : process.cwd();
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
- throw new Error(`Unknown command: ${command}\nRun \`architectonic help\` for usage.`);
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "architectonic",
3
- "version": "0.0.4",
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",