architectonic 0.0.4 → 0.0.6

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,11 @@ 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.
18
+
19
+ Starting in `0.0.6`, it can also inspect drift with `status` and `diff`.
17
20
 
18
21
  ## Command shape
19
22
 
@@ -25,9 +28,13 @@ architectonic add identity
25
28
  architectonic add project
26
29
  architectonic add skills
27
30
  architectonic add teleology identity skills
31
+ architectonic init
28
32
  architectonic doctor
33
+ architectonic status
34
+ architectonic diff
29
35
  architectonic list
30
36
  architectonic update
37
+ architectonic remove
31
38
  ```
32
39
 
33
40
  `add` is explicit and leaves room for future verbs such as `doctor`, `list`,
@@ -45,8 +52,16 @@ npx architectonic add skills
45
52
  npx architectonic add teleology identity skills
46
53
  npx architectonic add skills --dir ./vendor
47
54
  npx architectonic add teleology --source npm
55
+ npx architectonic init MyWorkspace
56
+ npx architectonic init --preset company
48
57
  npx architectonic list
49
58
  npx architectonic doctor
59
+ npx architectonic doctor --fix
60
+ npx architectonic status
61
+ npx architectonic diff teleology
62
+ npx architectonic update
63
+ npx architectonic update --dry-run
64
+ npx architectonic remove skills
50
65
  ```
51
66
 
52
67
  `add` installs from the Architectonic GitHub organization into the current
@@ -87,7 +102,47 @@ ARCHITECTONIC_ADD_SOURCE # change the default source mode
87
102
  `list` reads `architectonic.json` and shows installed layers.
88
103
 
89
104
  `doctor` verifies that each recorded layer still exists and that the installed
90
- package metadata matches the expected layer.
105
+ package metadata matches the expected layer. `doctor --fix` repairs light
106
+ manifest drift such as stale package names or recoverable default paths.
107
+
108
+ `status` gives a read-only summary of each layer:
109
+
110
+ ```text
111
+ git layers: branch, dirty/clean, ahead/behind upstream
112
+ npm layers: installed version vs published version
113
+ ```
114
+
115
+ `diff <layer>` drills into one layer:
116
+
117
+ ```text
118
+ git layers: local status lines plus ahead/behind numbers
119
+ npm layers: installed version vs published version
120
+ ```
121
+
122
+ `init` creates a workspace root, installs a preset, and seeds a top-level
123
+ `README.md` and `AGENTS.md`.
124
+
125
+ Supported presets:
126
+
127
+ ```text
128
+ solo # teleology + identity + project + skills
129
+ company # teleology + project + skills
130
+ project # project + skills
131
+ agent # identity + skills
132
+ ```
133
+
134
+ `update` is conservative by design:
135
+
136
+ ```text
137
+ git layers: only fast-forward clean git worktrees
138
+ npm layers: report newer packages but do not overwrite local forks
139
+ ```
140
+
141
+ If a user has modified an installed instance, `update` should skip it rather
142
+ than flatten their divergence.
143
+
144
+ `remove` deletes a recorded layer and updates the manifest. If the layer is a
145
+ dirty git worktree, it refuses unless `--force` is explicit.
91
146
 
92
147
  ## Run vs install
93
148
 
@@ -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.6";
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,14 @@ 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 status
38
+ npx architectonic diff <layer>
39
+ npx architectonic update
40
+ npx architectonic remove <layer>
35
41
 
36
42
  Layers:
37
43
  teleology purpose, principles, doctrine, governance
@@ -46,7 +52,14 @@ Run vs install:
46
52
 
47
53
  What add does:
48
54
  Installs the selected layer repositories into the target directory
49
- from git or npm sources and records them in architectonic.json.`);
55
+ from git or npm sources and records them in architectonic.json.
56
+
57
+ What status and diff do:
58
+ Inspect local drift from recorded sources without mutating anything.`);
59
+ }
60
+
61
+ function printUsageError(message) {
62
+ throw new Error(`${message}\nRun \`architectonic help\` for usage.`);
50
63
  }
51
64
 
52
65
  function parseAddArgs(tokens) {
@@ -95,6 +108,203 @@ function parseAddArgs(tokens) {
95
108
  return { targets, installDir, source };
96
109
  }
97
110
 
111
+ function parseInitArgs(tokens) {
112
+ let installDir = process.cwd();
113
+ let source = process.env.ARCHITECTONIC_ADD_SOURCE || "git";
114
+ let preset = "solo";
115
+ let workspaceName = "";
116
+
117
+ for (let index = 0; index < tokens.length; index += 1) {
118
+ const token = tokens[index];
119
+ if (token === "--dir" || token === "--out") {
120
+ const next = tokens[index + 1];
121
+ if (!next) {
122
+ throw new Error(`Missing value for ${token}`);
123
+ }
124
+ installDir = path.resolve(next);
125
+ index += 1;
126
+ continue;
127
+ }
128
+ if (token.startsWith("--dir=")) {
129
+ installDir = path.resolve(token.slice("--dir=".length));
130
+ continue;
131
+ }
132
+ if (token.startsWith("--out=")) {
133
+ installDir = path.resolve(token.slice("--out=".length));
134
+ continue;
135
+ }
136
+ if (token === "--source") {
137
+ const next = tokens[index + 1];
138
+ if (!next) {
139
+ throw new Error("Missing value for --source");
140
+ }
141
+ source = next;
142
+ index += 1;
143
+ continue;
144
+ }
145
+ if (token.startsWith("--source=")) {
146
+ source = token.slice("--source=".length);
147
+ continue;
148
+ }
149
+ if (token === "--preset") {
150
+ const next = tokens[index + 1];
151
+ if (!next) {
152
+ throw new Error("Missing value for --preset");
153
+ }
154
+ preset = next;
155
+ index += 1;
156
+ continue;
157
+ }
158
+ if (token.startsWith("--preset=")) {
159
+ preset = token.slice("--preset=".length);
160
+ continue;
161
+ }
162
+ if (token.startsWith("-")) {
163
+ throw new Error(`Unknown option: ${token}`);
164
+ }
165
+ if (!workspaceName) {
166
+ workspaceName = token;
167
+ continue;
168
+ }
169
+ throw new Error(`Unexpected argument: ${token}`);
170
+ }
171
+
172
+ if (workspaceName) {
173
+ installDir = path.resolve(installDir, workspaceName);
174
+ }
175
+
176
+ return { installDir, source, preset, workspaceName };
177
+ }
178
+
179
+ function parseDoctorArgs(tokens) {
180
+ let installDir = process.cwd();
181
+ let fix = false;
182
+
183
+ for (let index = 0; index < tokens.length; index += 1) {
184
+ const token = tokens[index];
185
+ if (token === "--fix") {
186
+ fix = true;
187
+ continue;
188
+ }
189
+ if (token.startsWith("-")) {
190
+ throw new Error(`Unknown option: ${token}`);
191
+ }
192
+ installDir = path.resolve(token);
193
+ }
194
+
195
+ return { installDir, fix };
196
+ }
197
+
198
+ function parseUpdateArgs(tokens) {
199
+ let installDir = process.cwd();
200
+ let dryRun = false;
201
+
202
+ for (let index = 0; index < tokens.length; index += 1) {
203
+ const token = tokens[index];
204
+ if (token === "--dry-run") {
205
+ dryRun = true;
206
+ continue;
207
+ }
208
+ if (token.startsWith("-")) {
209
+ throw new Error(`Unknown option: ${token}`);
210
+ }
211
+ installDir = path.resolve(token);
212
+ }
213
+
214
+ return { installDir, dryRun };
215
+ }
216
+
217
+ function parseStatusArgs(tokens) {
218
+ let installDir = process.cwd();
219
+
220
+ for (let index = 0; index < tokens.length; index += 1) {
221
+ const token = tokens[index];
222
+ if (token.startsWith("-")) {
223
+ throw new Error(`Unknown option: ${token}`);
224
+ }
225
+ installDir = path.resolve(token);
226
+ }
227
+
228
+ return { installDir };
229
+ }
230
+
231
+ function parseDiffArgs(tokens) {
232
+ let installDir = process.cwd();
233
+ let target = "";
234
+
235
+ for (let index = 0; index < tokens.length; index += 1) {
236
+ const token = tokens[index];
237
+ if (token === "--dir" || token === "--out") {
238
+ const next = tokens[index + 1];
239
+ if (!next) {
240
+ throw new Error(`Missing value for ${token}`);
241
+ }
242
+ installDir = path.resolve(next);
243
+ index += 1;
244
+ continue;
245
+ }
246
+ if (token.startsWith("--dir=")) {
247
+ installDir = path.resolve(token.slice("--dir=".length));
248
+ continue;
249
+ }
250
+ if (token.startsWith("--out=")) {
251
+ installDir = path.resolve(token.slice("--out=".length));
252
+ continue;
253
+ }
254
+ if (token.startsWith("-")) {
255
+ throw new Error(`Unknown option: ${token}`);
256
+ }
257
+ if (!target) {
258
+ target = token;
259
+ continue;
260
+ }
261
+ throw new Error(`Unexpected argument: ${token}`);
262
+ }
263
+
264
+ return { installDir, target };
265
+ }
266
+
267
+ function parseRemoveArgs(tokens) {
268
+ let installDir = process.cwd();
269
+ let force = false;
270
+ let target = "";
271
+
272
+ for (let index = 0; index < tokens.length; index += 1) {
273
+ const token = tokens[index];
274
+ if (token === "--force") {
275
+ force = true;
276
+ continue;
277
+ }
278
+ if (token === "--dir" || token === "--out") {
279
+ const next = tokens[index + 1];
280
+ if (!next) {
281
+ throw new Error(`Missing value for ${token}`);
282
+ }
283
+ installDir = path.resolve(next);
284
+ index += 1;
285
+ continue;
286
+ }
287
+ if (token.startsWith("--dir=")) {
288
+ installDir = path.resolve(token.slice("--dir=".length));
289
+ continue;
290
+ }
291
+ if (token.startsWith("--out=")) {
292
+ installDir = path.resolve(token.slice("--out=".length));
293
+ continue;
294
+ }
295
+ if (token.startsWith("-")) {
296
+ throw new Error(`Unknown option: ${token}`);
297
+ }
298
+ if (!target) {
299
+ target = token;
300
+ continue;
301
+ }
302
+ throw new Error(`Unexpected argument: ${token}`);
303
+ }
304
+
305
+ return { installDir, force, target };
306
+ }
307
+
98
308
  function ensureGitAvailable() {
99
309
  const result = spawnSync("git", ["--version"], { encoding: "utf8" });
100
310
  if (result.status !== 0) {
@@ -165,6 +375,14 @@ function writeManifest(manifestPath, manifest) {
165
375
  fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
166
376
  }
167
377
 
378
+ function readJson(filePath) {
379
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
380
+ }
381
+
382
+ function pathExists(filePath) {
383
+ return fs.existsSync(filePath);
384
+ }
385
+
168
386
  function cloneLayer(target, installDir) {
169
387
  const repoUrl = repoUrlFor(target);
170
388
  const targetPath = targetPathFor(installDir, target);
@@ -261,51 +479,170 @@ function readInstalledPackageName(layerPath) {
261
479
  return null;
262
480
  }
263
481
  try {
264
- return JSON.parse(fs.readFileSync(packageJsonPath, "utf8")).name || null;
482
+ return readJson(packageJsonPath).name || null;
265
483
  } catch {
266
484
  return null;
267
485
  }
268
486
  }
269
487
 
270
- function installLayer(target, installDir, source) {
488
+ function readInstalledVersion(layerPath) {
489
+ const packageJsonPath = path.join(layerPath, "package.json");
490
+ if (!fs.existsSync(packageJsonPath)) {
491
+ return null;
492
+ }
493
+ try {
494
+ return readJson(packageJsonPath).version || null;
495
+ } catch {
496
+ return null;
497
+ }
498
+ }
499
+
500
+ function hasGitRepo(layerPath) {
501
+ return pathExists(path.join(layerPath, ".git"));
502
+ }
503
+
504
+ function gitResult(args, cwd) {
505
+ return spawnSync("git", args, {
506
+ cwd,
507
+ encoding: "utf8",
508
+ stdio: "pipe",
509
+ });
510
+ }
511
+
512
+ function isGitWorktreeDirty(layerPath) {
513
+ const result = gitResult(["status", "--porcelain"], layerPath);
514
+ if (result.status !== 0) {
515
+ return null;
516
+ }
517
+ return Boolean((result.stdout || "").trim());
518
+ }
519
+
520
+ function packageMetaFor(target, layerPath) {
521
+ return {
522
+ expectedPackageName: packageMap[target] || null,
523
+ packageName: readInstalledPackageName(layerPath),
524
+ version: readInstalledVersion(layerPath),
525
+ };
526
+ }
527
+
528
+ function relativeManifestPath(installDir, absolutePath) {
529
+ return `./${path.relative(installDir, absolutePath).replace(/\\/g, "/")}`;
530
+ }
531
+
532
+ function ensureInstallPrereqs(source) {
271
533
  if (source === "git") {
272
- return cloneLayer(target, installDir);
534
+ ensureGitAvailable();
535
+ return;
273
536
  }
274
537
  if (source === "npm") {
275
- return packNpmLayer(target, installDir);
538
+ ensureNpmAvailable();
539
+ ensureTarAvailable();
540
+ return;
276
541
  }
277
542
  throw new Error(`Unsupported source: ${source}. Use git or npm.`);
278
543
  }
279
544
 
280
- function loadManifestFromDir(installDir) {
281
- const manifestPath = manifestPathFor(installDir);
282
- if (!fs.existsSync(manifestPath)) {
283
- throw new Error(`No architectonic.json found in ${installDir}`);
545
+ function updateManifestLayerRecord(manifest, installDir, layerName, absoluteLayerPath, source, refOverride = null) {
546
+ manifest.layers[layerName] = {
547
+ source,
548
+ ref: refOverride ?? manifest.layers[layerName]?.ref ?? (source === "git" ? repoUrlFor(layerName) : packageSpecFor(layerName)),
549
+ path: relativeManifestPath(installDir, absoluteLayerPath),
550
+ installed_at: new Date().toISOString(),
551
+ package_name: readInstalledPackageName(absoluteLayerPath),
552
+ };
553
+ }
554
+
555
+ function getGitHead(layerPath) {
556
+ const result = gitResult(["rev-parse", "HEAD"], layerPath);
557
+ if (result.status !== 0) {
558
+ return null;
284
559
  }
285
- return { manifestPath, manifest: readManifest(manifestPath) };
560
+ return (result.stdout || "").trim() || null;
286
561
  }
287
562
 
288
- function addCommand(tokens) {
289
- const { targets, installDir, source } = parseAddArgs(tokens);
563
+ function getGitBranch(layerPath) {
564
+ const result = gitResult(["rev-parse", "--abbrev-ref", "HEAD"], layerPath);
565
+ if (result.status !== 0) {
566
+ return null;
567
+ }
568
+ return (result.stdout || "").trim() || null;
569
+ }
290
570
 
291
- if (!targets.length) {
292
- throw new Error("Specify one or more layers: teleology, identity, project, skills");
571
+ function getGitUpstream(layerPath) {
572
+ const result = gitResult(["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"], layerPath);
573
+ if (result.status !== 0) {
574
+ return null;
293
575
  }
576
+ return (result.stdout || "").trim() || null;
577
+ }
294
578
 
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(", ")}`);
579
+ function getGitAheadBehind(layerPath) {
580
+ const upstream = getGitUpstream(layerPath);
581
+ if (!upstream) {
582
+ return null;
298
583
  }
584
+ const result = gitResult(["rev-list", "--left-right", "--count", `${upstream}...HEAD`], layerPath);
585
+ if (result.status !== 0) {
586
+ return null;
587
+ }
588
+ const [behindRaw, aheadRaw] = (result.stdout || "").trim().split(/\s+/);
589
+ return {
590
+ upstream,
591
+ behind: Number.parseInt(behindRaw || "0", 10),
592
+ ahead: Number.parseInt(aheadRaw || "0", 10),
593
+ };
594
+ }
299
595
 
300
- if (source === "git") {
301
- ensureGitAvailable();
302
- } else if (source === "npm") {
303
- ensureNpmAvailable();
304
- ensureTarAvailable();
305
- } else {
306
- throw new Error(`Unsupported source: ${source}. Use git or npm.`);
596
+ function getGitDiffSummary(layerPath) {
597
+ const result = gitResult(["status", "--short"], layerPath);
598
+ if (result.status !== 0) {
599
+ return null;
307
600
  }
601
+ const lines = (result.stdout || "").split(/\r?\n/).filter(Boolean);
602
+ return {
603
+ dirty: lines.length > 0,
604
+ lines,
605
+ };
606
+ }
308
607
 
608
+ function getNpmPublishedVersion(packageSpec) {
609
+ const result = spawnSync("npm", ["view", packageSpec, "version"], {
610
+ encoding: "utf8",
611
+ stdio: "pipe",
612
+ shell: true,
613
+ });
614
+ if (result.status !== 0) {
615
+ return null;
616
+ }
617
+ return (result.stdout || "").trim() || null;
618
+ }
619
+
620
+ function describeGitLayerState(layerPath) {
621
+ const branch = getGitBranch(layerPath);
622
+ const head = getGitHead(layerPath);
623
+ const diffSummary = getGitDiffSummary(layerPath);
624
+ const aheadBehind = getGitAheadBehind(layerPath);
625
+ return {
626
+ branch,
627
+ head,
628
+ dirty: diffSummary?.dirty ?? false,
629
+ diffLines: diffSummary?.lines ?? [],
630
+ aheadBehind,
631
+ };
632
+ }
633
+
634
+ function describeNpmLayerState(layerName, layerPath) {
635
+ const installedVersion = readInstalledVersion(layerPath);
636
+ const publishedVersion = getNpmPublishedVersion(packageSpecFor(layerName));
637
+ return {
638
+ installedVersion,
639
+ publishedVersion,
640
+ outdated: Boolean(installedVersion && publishedVersion && installedVersion !== publishedVersion),
641
+ };
642
+ }
643
+
644
+ function installTargets(targets, installDir, source) {
645
+ ensureInstallPrereqs(source);
309
646
  fs.mkdirSync(installDir, { recursive: true });
310
647
 
311
648
  const installed = [];
@@ -322,13 +659,7 @@ function addCommand(tokens) {
322
659
  manifest.npm_source_base = npmBase || "registry";
323
660
 
324
661
  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
- };
662
+ updateManifestLayerRecord(manifest, installDir, item.name, item.path, item.source, item.repo);
332
663
  }
333
664
 
334
665
  writeManifest(manifestPath, manifest);
@@ -338,6 +669,86 @@ function addCommand(tokens) {
338
669
  console.log(`Wrote ${manifestPath}`);
339
670
  }
340
671
 
672
+ function installLayer(target, installDir, source) {
673
+ if (source === "git") {
674
+ return cloneLayer(target, installDir);
675
+ }
676
+ if (source === "npm") {
677
+ return packNpmLayer(target, installDir);
678
+ }
679
+ throw new Error(`Unsupported source: ${source}. Use git or npm.`);
680
+ }
681
+
682
+ function loadManifestFromDir(installDir) {
683
+ const manifestPath = manifestPathFor(installDir);
684
+ if (!fs.existsSync(manifestPath)) {
685
+ throw new Error(`No architectonic.json found in ${installDir}`);
686
+ }
687
+ return { manifestPath, manifest: readManifest(manifestPath) };
688
+ }
689
+
690
+ function addCommand(tokens) {
691
+ const { targets, installDir, source } = parseAddArgs(tokens);
692
+
693
+ if (!targets.length) {
694
+ throw new Error("Specify one or more layers: teleology, identity, project, skills");
695
+ }
696
+
697
+ const invalid = targets.filter((target) => !supportedSet.has(target));
698
+ if (invalid.length) {
699
+ throw new Error(`Unknown layer(s): ${invalid.join(", ")}\nSupported layers: ${supported.join(", ")}`);
700
+ }
701
+
702
+ installTargets(targets, installDir, source);
703
+ }
704
+
705
+ function resolvePreset(preset) {
706
+ const presets = {
707
+ solo: ["teleology", "identity", "project", "skills"],
708
+ company: ["teleology", "project", "skills"],
709
+ project: ["project", "skills"],
710
+ agent: ["identity", "skills"],
711
+ };
712
+ const layers = presets[preset];
713
+ if (!layers) {
714
+ throw new Error(`Unknown preset: ${preset}. Supported presets: ${Object.keys(presets).join(", ")}`);
715
+ }
716
+ return layers;
717
+ }
718
+
719
+ function writeInitFiles(installDir, workspaceName, layers) {
720
+ const displayName = workspaceName || path.basename(installDir);
721
+ const readmePath = path.join(installDir, "README.md");
722
+ const agentsPath = path.join(installDir, "AGENTS.md");
723
+
724
+ if (!pathExists(readmePath)) {
725
+ fs.writeFileSync(
726
+ readmePath,
727
+ `# ${displayName}\n\nThis workspace was initialized by \`architectonic\`.\n\nInstalled layers:\n\n${layers.map((layer) => `- ${layer}`).join("\n")}\n`,
728
+ "utf8",
729
+ );
730
+ }
731
+
732
+ if (!pathExists(agentsPath)) {
733
+ fs.writeFileSync(
734
+ agentsPath,
735
+ `# Agent Instructions\n\nRead installed layers before making structural changes.\n\nPriority order:\n1. teleology\n2. identity\n3. project\n4. skills\n`,
736
+ "utf8",
737
+ );
738
+ }
739
+ }
740
+
741
+ function initCommand(tokens) {
742
+ const { installDir, source, preset, workspaceName } = parseInitArgs(tokens);
743
+ if (pathExists(installDir) && fs.readdirSync(installDir).length > 0) {
744
+ throw new Error(`Refusing to initialize into a non-empty directory: ${installDir}`);
745
+ }
746
+ fs.mkdirSync(installDir, { recursive: true });
747
+ const layers = resolvePreset(preset);
748
+ writeInitFiles(installDir, workspaceName, layers);
749
+ installTargets(layers, installDir, source);
750
+ }
751
+
341
752
  function listCommand(tokens) {
342
753
  const installDir = tokens[0] ? path.resolve(tokens[0]) : process.cwd();
343
754
  const { manifest } = loadManifestFromDir(installDir);
@@ -359,11 +770,65 @@ function listCommand(tokens) {
359
770
  }
360
771
  }
361
772
 
773
+ function statusCommand(tokens) {
774
+ const { installDir } = parseStatusArgs(tokens);
775
+ const { manifest } = loadManifestFromDir(installDir);
776
+ const entries = Object.entries(manifest.layers || {});
777
+
778
+ if (!entries.length) {
779
+ console.log("No layers installed.");
780
+ return;
781
+ }
782
+
783
+ ensureGitAvailable();
784
+ ensureNpmAvailable();
785
+
786
+ console.log(`architectonic status`);
787
+ console.log(` root: ${installDir}`);
788
+
789
+ for (const [name, layer] of entries) {
790
+ const layerPath = path.resolve(installDir, String(layer.path || ""));
791
+ if (!pathExists(layerPath)) {
792
+ console.log(` [missing] ${name}: ${layer.path || "unknown path"}`);
793
+ continue;
794
+ }
795
+
796
+ if (layer.source === "git") {
797
+ const state = describeGitLayerState(layerPath);
798
+ const dirtyLabel = state.dirty ? "dirty" : "clean";
799
+ const branchLabel = state.branch || "detached";
800
+ let relation = "no-upstream";
801
+ if (state.aheadBehind) {
802
+ relation = `ahead ${state.aheadBehind.ahead}, behind ${state.aheadBehind.behind}`;
803
+ }
804
+ console.log(` [git] ${name}: ${branchLabel}, ${dirtyLabel}, ${relation}`);
805
+ continue;
806
+ }
807
+
808
+ if (layer.source === "npm") {
809
+ const state = describeNpmLayerState(name, layerPath);
810
+ const installed = state.installedVersion || "unknown";
811
+ const published = state.publishedVersion || "unknown";
812
+ const relation = state.outdated ? "outdated" : "current-or-unknown";
813
+ console.log(` [npm] ${name}: installed ${installed}, published ${published}, ${relation}`);
814
+ continue;
815
+ }
816
+
817
+ console.log(` [unknown] ${name}: unsupported source ${layer.source || "unknown"}`);
818
+ }
819
+ }
820
+
821
+ function inferLayerPath(installDir, layerName) {
822
+ const candidate = targetPathFor(installDir, layerName);
823
+ return pathExists(candidate) ? candidate : null;
824
+ }
825
+
362
826
  function doctorCommand(tokens) {
363
- const installDir = tokens[0] ? path.resolve(tokens[0]) : process.cwd();
827
+ const { installDir, fix } = parseDoctorArgs(tokens);
364
828
  const { manifestPath, manifest } = loadManifestFromDir(installDir);
365
829
  const entries = Object.entries(manifest.layers || {});
366
830
  let failures = 0;
831
+ let changed = false;
367
832
 
368
833
  console.log(`architectonic doctor`);
369
834
  console.log(` root: ${installDir}`);
@@ -384,6 +849,13 @@ function doctorCommand(tokens) {
384
849
  const expectedPackageName = packageMap[name];
385
850
 
386
851
  if (!exists) {
852
+ const inferredPath = inferLayerPath(installDir, name);
853
+ if (fix && inferredPath) {
854
+ updateManifestLayerRecord(manifest, installDir, name, inferredPath, layer.source || "unknown", layer.ref || null);
855
+ changed = true;
856
+ console.log(` [fix] ${name}: repointed path to ${relativeManifestPath(installDir, inferredPath)}`);
857
+ continue;
858
+ }
387
859
  console.log(` [fail] ${name}: missing directory ${layerPath}`);
388
860
  failures += 1;
389
861
  continue;
@@ -396,25 +868,238 @@ function doctorCommand(tokens) {
396
868
  }
397
869
 
398
870
  if (packageName && expectedPackageName && packageName !== expectedPackageName) {
871
+ if (fix) {
872
+ manifest.layers[name].package_name = packageName;
873
+ manifest.layers[name].path = relativeManifestPath(installDir, layerPath);
874
+ changed = true;
875
+ console.log(` [fix] ${name}: synced manifest package name to ${packageName}`);
876
+ continue;
877
+ }
399
878
  console.log(` [fail] ${name}: expected package ${expectedPackageName}, found ${packageName}`);
400
879
  failures += 1;
401
880
  continue;
402
881
  }
403
882
 
883
+ if (fix) {
884
+ const normalizedPath = relativeManifestPath(installDir, layerPath);
885
+ if (manifest.layers[name].path !== normalizedPath || manifest.layers[name].package_name !== packageName) {
886
+ manifest.layers[name].path = normalizedPath;
887
+ manifest.layers[name].package_name = packageName;
888
+ changed = true;
889
+ }
890
+ }
891
+
404
892
  console.log(` [ok] ${name}: ${relativePath} (${layer.source || "unknown"})`);
405
893
  }
406
894
 
895
+ if (fix && changed) {
896
+ writeManifest(manifestPath, manifest);
897
+ console.log(` [write] updated manifest repairs`);
898
+ }
899
+
407
900
  if (failures > 0) {
408
901
  process.exit(1);
409
902
  }
410
903
  }
411
904
 
905
+ function updateGitLayer(layerName, layerPath, dryRun) {
906
+ if (!hasGitRepo(layerPath)) {
907
+ return { status: "skip", detail: "not a git repository" };
908
+ }
909
+ const dirty = isGitWorktreeDirty(layerPath);
910
+ if (dirty === null) {
911
+ return { status: "skip", detail: "unable to inspect git status" };
912
+ }
913
+ if (dirty) {
914
+ return { status: "skip", detail: "local changes detected; preserving fork" };
915
+ }
916
+ if (dryRun) {
917
+ return { status: "plan", detail: "would run git pull --ff-only" };
918
+ }
919
+ const result = gitResult(["pull", "--ff-only"], layerPath);
920
+ if (result.status !== 0) {
921
+ return { status: "fail", detail: (result.stderr || result.stdout || "").trim() || "git pull failed" };
922
+ }
923
+ return { status: "ok", detail: (result.stdout || "").trim() || "up to date" };
924
+ }
925
+
926
+ function getLatestNpmVersion(packageSpec) {
927
+ const result = spawnSync("npm", ["view", packageSpec, "version"], {
928
+ encoding: "utf8",
929
+ stdio: "pipe",
930
+ shell: true,
931
+ });
932
+ if (result.status !== 0) {
933
+ return null;
934
+ }
935
+ return (result.stdout || "").trim() || null;
936
+ }
937
+
938
+ function updateNpmLayer(layerName, layerPath) {
939
+ const installedVersion = readInstalledVersion(layerPath);
940
+ const packageSpec = packageSpecFor(layerName);
941
+ const latestVersion = getLatestNpmVersion(packageSpec);
942
+ if (!latestVersion) {
943
+ return { status: "skip", detail: "unable to resolve latest npm version" };
944
+ }
945
+ if (!installedVersion) {
946
+ return { status: "skip", detail: `latest ${latestVersion} available; local version unknown` };
947
+ }
948
+ if (installedVersion === latestVersion) {
949
+ return { status: "ok", detail: `already at ${installedVersion}` };
950
+ }
951
+ return {
952
+ status: "skip",
953
+ detail: `newer package ${latestVersion} available; skipped to preserve local fork (${installedVersion} installed)`,
954
+ };
955
+ }
956
+
957
+ function updateCommand(tokens) {
958
+ const { installDir, dryRun } = parseUpdateArgs(tokens);
959
+ const { manifestPath, manifest } = loadManifestFromDir(installDir);
960
+ const entries = Object.entries(manifest.layers || {});
961
+
962
+ if (!entries.length) {
963
+ throw new Error("No installed layers recorded.");
964
+ }
965
+
966
+ ensureGitAvailable();
967
+ ensureNpmAvailable();
968
+
969
+ let changed = false;
970
+ console.log(`architectonic update${dryRun ? " --dry-run" : ""}`);
971
+ console.log(` root: ${installDir}`);
972
+
973
+ for (const [name, layer] of entries) {
974
+ const layerPath = path.resolve(installDir, String(layer.path || ""));
975
+ if (!pathExists(layerPath)) {
976
+ console.log(` [skip] ${name}: missing directory`);
977
+ continue;
978
+ }
979
+
980
+ let outcome;
981
+ if (layer.source === "git") {
982
+ outcome = updateGitLayer(name, layerPath, dryRun);
983
+ } else if (layer.source === "npm") {
984
+ outcome = updateNpmLayer(name, layerPath);
985
+ } else {
986
+ outcome = { status: "skip", detail: `unsupported source ${layer.source || "unknown"}` };
987
+ }
988
+
989
+ console.log(` [${outcome.status}] ${name}: ${outcome.detail}`);
990
+ if (outcome.status === "ok" && !dryRun) {
991
+ manifest.layers[name].installed_at = new Date().toISOString();
992
+ manifest.layers[name].package_name = readInstalledPackageName(layerPath);
993
+ changed = true;
994
+ }
995
+ }
996
+
997
+ if (changed) {
998
+ writeManifest(manifestPath, manifest);
999
+ console.log(` [write] refreshed manifest timestamps`);
1000
+ }
1001
+ }
1002
+
1003
+ function removeCommand(tokens) {
1004
+ const { installDir, force, target } = parseRemoveArgs(tokens);
1005
+ if (!target) {
1006
+ throw new Error("Specify a layer to remove.");
1007
+ }
1008
+ if (!supportedSet.has(target)) {
1009
+ throw new Error(`Unknown layer: ${target}`);
1010
+ }
1011
+ const { manifestPath, manifest } = loadManifestFromDir(installDir);
1012
+ const layer = manifest.layers[target];
1013
+ if (!layer) {
1014
+ throw new Error(`Layer not recorded in manifest: ${target}`);
1015
+ }
1016
+ const layerPath = path.resolve(installDir, String(layer.path || ""));
1017
+ if (pathExists(layerPath) && hasGitRepo(layerPath) && !force) {
1018
+ const dirty = isGitWorktreeDirty(layerPath);
1019
+ if (dirty) {
1020
+ throw new Error(`Refusing to remove ${target}: local git changes detected. Re-run with --force if you really want to delete it.`);
1021
+ }
1022
+ }
1023
+ if (pathExists(layerPath)) {
1024
+ fs.rmSync(layerPath, { recursive: true, force: true });
1025
+ }
1026
+ delete manifest.layers[target];
1027
+ writeManifest(manifestPath, manifest);
1028
+ console.log(`Removed ${target}`);
1029
+ console.log(`Updated ${manifestPath}`);
1030
+ }
1031
+
1032
+ function diffCommand(tokens) {
1033
+ const { installDir, target } = parseDiffArgs(tokens);
1034
+ if (!target) {
1035
+ throw new Error("Specify a layer to diff.");
1036
+ }
1037
+ if (!supportedSet.has(target)) {
1038
+ throw new Error(`Unknown layer: ${target}`);
1039
+ }
1040
+ const { manifest } = loadManifestFromDir(installDir);
1041
+ const layer = manifest.layers[target];
1042
+ if (!layer) {
1043
+ throw new Error(`Layer not recorded in manifest: ${target}`);
1044
+ }
1045
+ const layerPath = path.resolve(installDir, String(layer.path || ""));
1046
+ if (!pathExists(layerPath)) {
1047
+ throw new Error(`Layer path does not exist: ${layerPath}`);
1048
+ }
1049
+
1050
+ if (layer.source === "git") {
1051
+ ensureGitAvailable();
1052
+ const summary = getGitDiffSummary(layerPath);
1053
+ const aheadBehind = getGitAheadBehind(layerPath);
1054
+ console.log(`architectonic diff ${target}`);
1055
+ console.log(` source: git`);
1056
+ console.log(` path: ${layerPath}`);
1057
+ if (aheadBehind) {
1058
+ console.log(` upstream: ${aheadBehind.upstream}`);
1059
+ console.log(` ahead: ${aheadBehind.ahead}`);
1060
+ console.log(` behind: ${aheadBehind.behind}`);
1061
+ }
1062
+ if (!summary || !summary.lines.length) {
1063
+ console.log(` local changes: none`);
1064
+ return;
1065
+ }
1066
+ console.log(` local changes:`);
1067
+ for (const line of summary.lines) {
1068
+ console.log(` ${line}`);
1069
+ }
1070
+ return;
1071
+ }
1072
+
1073
+ if (layer.source === "npm") {
1074
+ ensureNpmAvailable();
1075
+ const state = describeNpmLayerState(target, layerPath);
1076
+ console.log(`architectonic diff ${target}`);
1077
+ console.log(` source: npm`);
1078
+ console.log(` path: ${layerPath}`);
1079
+ console.log(` installed version: ${state.installedVersion || "unknown"}`);
1080
+ console.log(` published version: ${state.publishedVersion || "unknown"}`);
1081
+ if (state.outdated) {
1082
+ console.log(` drift: newer package available`);
1083
+ } else {
1084
+ console.log(` drift: none detected from package version`);
1085
+ }
1086
+ return;
1087
+ }
1088
+
1089
+ throw new Error(`Unsupported source for diff: ${layer.source || "unknown"}`);
1090
+ }
1091
+
412
1092
  try {
413
1093
  if (!command || command === "help" || command === "--help" || command === "-h") {
414
1094
  printHelp();
415
1095
  process.exit(0);
416
1096
  }
417
1097
 
1098
+ if (command === "init") {
1099
+ initCommand(rest);
1100
+ process.exit(0);
1101
+ }
1102
+
418
1103
  if (command === "add") {
419
1104
  addCommand(rest);
420
1105
  process.exit(0);
@@ -430,7 +1115,27 @@ try {
430
1115
  process.exit(0);
431
1116
  }
432
1117
 
433
- throw new Error(`Unknown command: ${command}\nRun \`architectonic help\` for usage.`);
1118
+ if (command === "status") {
1119
+ statusCommand(rest);
1120
+ process.exit(0);
1121
+ }
1122
+
1123
+ if (command === "diff") {
1124
+ diffCommand(rest);
1125
+ process.exit(0);
1126
+ }
1127
+
1128
+ if (command === "update") {
1129
+ updateCommand(rest);
1130
+ process.exit(0);
1131
+ }
1132
+
1133
+ if (command === "remove") {
1134
+ removeCommand(rest);
1135
+ process.exit(0);
1136
+ }
1137
+
1138
+ printUsageError(`Unknown command: ${command}`);
434
1139
  } catch (error) {
435
1140
  console.error(error.message);
436
1141
  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.6",
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",