architectonic 0.0.3 → 0.0.4

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,8 @@ 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.4`, `architectonic add` installs the selected layer repositories from
16
+ either git or npm sources and records them in `architectonic.json`.
17
17
 
18
18
  ## Command shape
19
19
 
@@ -44,9 +44,12 @@ npx architectonic add project
44
44
  npx architectonic add skills
45
45
  npx architectonic add teleology identity skills
46
46
  npx architectonic add skills --dir ./vendor
47
+ npx architectonic add teleology --source npm
48
+ npx architectonic list
49
+ npx architectonic doctor
47
50
  ```
48
51
 
49
- `add` clones from the Architectonic GitHub organization into the current
52
+ `add` installs from the Architectonic GitHub organization into the current
50
53
  directory by default:
51
54
 
52
55
  ```text
@@ -62,6 +65,30 @@ directory by default:
62
65
  If a target directory already exists, the command stops instead of silently
63
66
  overwriting it.
64
67
 
68
+ ## Sources
69
+
70
+ `add` supports two source modes:
71
+
72
+ ```text
73
+ --source git # clone from GitHub or another git base
74
+ --source npm # pack and extract from npm packages
75
+ ```
76
+
77
+ The default is `git`.
78
+
79
+ Environment overrides:
80
+
81
+ ```text
82
+ ARCHITECTONIC_SOURCE_BASE # override the git source base
83
+ ARCHITECTONIC_NPM_BASE # override the npm package base or local package root
84
+ ARCHITECTONIC_ADD_SOURCE # change the default source mode
85
+ ```
86
+
87
+ `list` reads `architectonic.json` and shows installed layers.
88
+
89
+ `doctor` verifies that each recorded layer still exists and that the installed
90
+ package metadata matches the expected layer.
91
+
65
92
  ## Run vs install
66
93
 
67
94
  ```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.4";
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,9 @@ 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 list
34
+ npx architectonic doctor
25
35
 
26
36
  Layers:
27
37
  teleology purpose, principles, doctrine, governance
@@ -35,13 +45,14 @@ Run vs install:
35
45
  npm install -g architectonic install globally
36
46
 
37
47
  What add does:
38
- Clones the selected layer repositories into the target directory
39
- and records them in architectonic.json.`);
48
+ Installs the selected layer repositories into the target directory
49
+ from git or npm sources and records them in architectonic.json.`);
40
50
  }
41
51
 
42
52
  function parseAddArgs(tokens) {
43
53
  const targets = [];
44
54
  let installDir = process.cwd();
55
+ let source = process.env.ARCHITECTONIC_ADD_SOURCE || "git";
45
56
 
46
57
  for (let index = 0; index < tokens.length; index += 1) {
47
58
  const token = tokens[index];
@@ -62,13 +73,26 @@ function parseAddArgs(tokens) {
62
73
  installDir = path.resolve(token.slice("--out=".length));
63
74
  continue;
64
75
  }
76
+ if (token === "--source") {
77
+ const next = tokens[index + 1];
78
+ if (!next) {
79
+ throw new Error("Missing value for --source");
80
+ }
81
+ source = next;
82
+ index += 1;
83
+ continue;
84
+ }
85
+ if (token.startsWith("--source=")) {
86
+ source = token.slice("--source=".length);
87
+ continue;
88
+ }
65
89
  if (token.startsWith("-")) {
66
90
  throw new Error(`Unknown option: ${token}`);
67
91
  }
68
92
  targets.push(token);
69
93
  }
70
94
 
71
- return { targets, installDir };
95
+ return { targets, installDir, source };
72
96
  }
73
97
 
74
98
  function ensureGitAvailable() {
@@ -78,6 +102,20 @@ function ensureGitAvailable() {
78
102
  }
79
103
  }
80
104
 
105
+ function ensureNpmAvailable() {
106
+ const result = spawnSync("npm", ["--version"], { encoding: "utf8", shell: true });
107
+ if (result.status !== 0) {
108
+ throw new Error("npm is required on PATH for `architectonic add --source npm`.");
109
+ }
110
+ }
111
+
112
+ function ensureTarAvailable() {
113
+ const result = spawnSync("tar", ["--version"], { encoding: "utf8" });
114
+ if (result.status !== 0) {
115
+ throw new Error("tar is required on PATH for npm package extraction.");
116
+ }
117
+ }
118
+
81
119
  function repoUrlFor(target) {
82
120
  const normalizedBase = repoBase.replace(/\\/g, "/");
83
121
  if (/^(?:[A-Za-z]:\/|\/|\.{1,2}\/)/.test(normalizedBase)) {
@@ -86,10 +124,31 @@ function repoUrlFor(target) {
86
124
  return `${normalizedBase}/${target}.git`;
87
125
  }
88
126
 
127
+ function packageSpecFor(target) {
128
+ const packageName = packageMap[target];
129
+ if (!packageName) {
130
+ throw new Error(`No npm package mapping defined for ${target}`);
131
+ }
132
+ if (!npmBase) {
133
+ return packageName;
134
+ }
135
+ const normalizedBase = npmBase.replace(/\\/g, "/");
136
+ if (/^(?:[A-Za-z]:\/|\/|\.{1,2}\/)/.test(normalizedBase)) {
137
+ return path.resolve(normalizedBase, target);
138
+ }
139
+ return normalizedBase.endsWith("/")
140
+ ? `${normalizedBase}${packageName}`
141
+ : `${normalizedBase}/${packageName}`;
142
+ }
143
+
89
144
  function targetPathFor(installDir, target) {
90
145
  return path.join(installDir, target);
91
146
  }
92
147
 
148
+ function manifestPathFor(installDir) {
149
+ return path.join(installDir, "architectonic.json");
150
+ }
151
+
93
152
  function readManifest(manifestPath) {
94
153
  if (!fs.existsSync(manifestPath)) {
95
154
  return {
@@ -129,11 +188,105 @@ function cloneLayer(target, installDir) {
129
188
  name: target,
130
189
  repo: repoUrl,
131
190
  path: targetPath,
191
+ source: "git",
132
192
  };
133
193
  }
134
194
 
195
+ function packNpmLayer(target, installDir) {
196
+ const packageSpec = packageSpecFor(target);
197
+ const targetPath = targetPathFor(installDir, target);
198
+ const tempRoot = fs.mkdtempSync(path.join(installDir, "architectonic-npm-"));
199
+ const pack = spawnSync("npm", ["pack", packageSpec, "--pack-destination", tempRoot], {
200
+ cwd: installDir,
201
+ encoding: "utf8",
202
+ stdio: "pipe",
203
+ shell: true,
204
+ });
205
+
206
+ if (pack.status !== 0) {
207
+ const detail = (pack.stderr || pack.stdout || "").trim();
208
+ throw new Error(`Failed to pack ${packageSpec}${detail ? `\n${detail}` : ""}`);
209
+ }
210
+
211
+ const tgzName = (pack.stdout || "").trim().split(/\r?\n/).filter(Boolean).pop();
212
+ if (!tgzName) {
213
+ throw new Error(`npm pack did not return a tarball name for ${packageSpec}`);
214
+ }
215
+
216
+ const tgzPath = path.join(tempRoot, tgzName);
217
+ if (!fs.existsSync(tgzPath)) {
218
+ throw new Error(`Packed tarball not found: ${tgzPath}`);
219
+ }
220
+
221
+ if (fs.existsSync(targetPath)) {
222
+ throw new Error(`Target already exists: ${targetPath}`);
223
+ }
224
+
225
+ fs.mkdirSync(targetPath, { recursive: true });
226
+ const extract = spawnSync("tar", ["-xzf", tgzPath, "-C", targetPath], {
227
+ cwd: installDir,
228
+ encoding: "utf8",
229
+ stdio: "pipe",
230
+ });
231
+
232
+ if (extract.status !== 0) {
233
+ const detail = (extract.stderr || extract.stdout || "").trim();
234
+ throw new Error(`Failed to extract ${tgzPath}${detail ? `\n${detail}` : ""}`);
235
+ }
236
+
237
+ const extractedPackageDir = path.join(targetPath, "package");
238
+ if (!fs.existsSync(extractedPackageDir)) {
239
+ throw new Error(`Expected extracted package directory at ${extractedPackageDir}`);
240
+ }
241
+
242
+ for (const entry of fs.readdirSync(extractedPackageDir, { withFileTypes: true })) {
243
+ const from = path.join(extractedPackageDir, entry.name);
244
+ const to = path.join(targetPath, entry.name);
245
+ fs.renameSync(from, to);
246
+ }
247
+ fs.rmdirSync(extractedPackageDir);
248
+ fs.rmSync(tempRoot, { recursive: true, force: true });
249
+
250
+ return {
251
+ name: target,
252
+ repo: packageSpec,
253
+ path: targetPath,
254
+ source: "npm",
255
+ };
256
+ }
257
+
258
+ function readInstalledPackageName(layerPath) {
259
+ const packageJsonPath = path.join(layerPath, "package.json");
260
+ if (!fs.existsSync(packageJsonPath)) {
261
+ return null;
262
+ }
263
+ try {
264
+ return JSON.parse(fs.readFileSync(packageJsonPath, "utf8")).name || null;
265
+ } catch {
266
+ return null;
267
+ }
268
+ }
269
+
270
+ function installLayer(target, installDir, source) {
271
+ if (source === "git") {
272
+ return cloneLayer(target, installDir);
273
+ }
274
+ if (source === "npm") {
275
+ return packNpmLayer(target, installDir);
276
+ }
277
+ throw new Error(`Unsupported source: ${source}. Use git or npm.`);
278
+ }
279
+
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) };
286
+ }
287
+
135
288
  function addCommand(tokens) {
136
- const { targets, installDir } = parseAddArgs(tokens);
289
+ const { targets, installDir, source } = parseAddArgs(tokens);
137
290
 
138
291
  if (!targets.length) {
139
292
  throw new Error("Specify one or more layers: teleology, identity, project, skills");
@@ -144,25 +297,37 @@ function addCommand(tokens) {
144
297
  throw new Error(`Unknown layer(s): ${invalid.join(", ")}\nSupported layers: ${supported.join(", ")}`);
145
298
  }
146
299
 
147
- ensureGitAvailable();
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.`);
307
+ }
308
+
148
309
  fs.mkdirSync(installDir, { recursive: true });
149
310
 
150
311
  const installed = [];
151
312
  for (const target of targets) {
152
- console.log(`Adding ${target}...`);
153
- installed.push(cloneLayer(target, installDir));
313
+ console.log(`Adding ${target} from ${source}...`);
314
+ installed.push(installLayer(target, installDir, source));
154
315
  }
155
316
 
156
- const manifestPath = path.join(installDir, "architectonic.json");
317
+ const manifestPath = manifestPathFor(installDir);
157
318
  const manifest = readManifest(manifestPath);
158
319
  manifest.installed_at = new Date().toISOString();
159
- manifest.source_base = repoBase;
320
+ manifest.last_source = source;
321
+ manifest.git_source_base = repoBase;
322
+ manifest.npm_source_base = npmBase || "registry";
160
323
 
161
324
  for (const item of installed) {
162
325
  manifest.layers[item.name] = {
163
- repo: item.repo,
326
+ source: item.source,
327
+ ref: item.repo,
164
328
  path: `./${path.relative(installDir, item.path).replace(/\\/g, "/")}`,
165
329
  installed_at: manifest.installed_at,
330
+ package_name: readInstalledPackageName(item.path),
166
331
  };
167
332
  }
168
333
 
@@ -173,6 +338,77 @@ function addCommand(tokens) {
173
338
  console.log(`Wrote ${manifestPath}`);
174
339
  }
175
340
 
341
+ function listCommand(tokens) {
342
+ const installDir = tokens[0] ? path.resolve(tokens[0]) : process.cwd();
343
+ const { manifest } = loadManifestFromDir(installDir);
344
+ const entries = Object.entries(manifest.layers || {});
345
+
346
+ if (!entries.length) {
347
+ console.log("No layers installed.");
348
+ return;
349
+ }
350
+
351
+ for (const [name, layer] of entries) {
352
+ console.log(`${name}`);
353
+ console.log(` source: ${layer.source || "unknown"}`);
354
+ console.log(` path: ${layer.path || "unknown"}`);
355
+ console.log(` ref: ${layer.ref || "unknown"}`);
356
+ if (layer.package_name) {
357
+ console.log(` pkg: ${layer.package_name}`);
358
+ }
359
+ }
360
+ }
361
+
362
+ function doctorCommand(tokens) {
363
+ const installDir = tokens[0] ? path.resolve(tokens[0]) : process.cwd();
364
+ const { manifestPath, manifest } = loadManifestFromDir(installDir);
365
+ const entries = Object.entries(manifest.layers || {});
366
+ let failures = 0;
367
+
368
+ console.log(`architectonic doctor`);
369
+ console.log(` root: ${installDir}`);
370
+ console.log(` manifest: ${manifestPath}`);
371
+
372
+ if (!entries.length) {
373
+ console.log(` [fail] no installed layers recorded`);
374
+ process.exit(1);
375
+ }
376
+
377
+ for (const [name, layer] of entries) {
378
+ const relativePath = String(layer.path || "");
379
+ const layerPath = path.resolve(installDir, relativePath);
380
+ const packageJsonPath = path.join(layerPath, "package.json");
381
+ const exists = fs.existsSync(layerPath);
382
+ const hasPackageJson = fs.existsSync(packageJsonPath);
383
+ const packageName = hasPackageJson ? readInstalledPackageName(layerPath) : null;
384
+ const expectedPackageName = packageMap[name];
385
+
386
+ if (!exists) {
387
+ console.log(` [fail] ${name}: missing directory ${layerPath}`);
388
+ failures += 1;
389
+ continue;
390
+ }
391
+
392
+ if (!hasPackageJson) {
393
+ console.log(` [fail] ${name}: missing package.json`);
394
+ failures += 1;
395
+ continue;
396
+ }
397
+
398
+ if (packageName && expectedPackageName && packageName !== expectedPackageName) {
399
+ console.log(` [fail] ${name}: expected package ${expectedPackageName}, found ${packageName}`);
400
+ failures += 1;
401
+ continue;
402
+ }
403
+
404
+ console.log(` [ok] ${name}: ${relativePath} (${layer.source || "unknown"})`);
405
+ }
406
+
407
+ if (failures > 0) {
408
+ process.exit(1);
409
+ }
410
+ }
411
+
176
412
  try {
177
413
  if (!command || command === "help" || command === "--help" || command === "-h") {
178
414
  printHelp();
@@ -184,6 +420,16 @@ try {
184
420
  process.exit(0);
185
421
  }
186
422
 
423
+ if (command === "list") {
424
+ listCommand(rest);
425
+ process.exit(0);
426
+ }
427
+
428
+ if (command === "doctor") {
429
+ doctorCommand(rest);
430
+ process.exit(0);
431
+ }
432
+
187
433
  throw new Error(`Unknown command: ${command}\nRun \`architectonic help\` for usage.`);
188
434
  } catch (error) {
189
435
  console.error(error.message);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "architectonic",
3
- "version": "0.0.3",
3
+ "version": "0.0.4",
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",