architectonic 0.0.2 → 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
@@ -11,9 +11,9 @@ project -- operating context for a concrete initiative
11
11
  skills -- reusable procedures and capabilities
12
12
  ```
13
13
 
14
- The initial placeholder releases are intentionally minimal. Their job is to reserve the
15
- package name and establish the top-level command surface for future install,
16
- cascade, and upkeep workflows.
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`.
17
17
 
18
18
  ## Command shape
19
19
 
@@ -35,16 +35,59 @@ architectonic update
35
35
 
36
36
  ## Current behavior
37
37
 
38
- For now, the CLI only exposes:
38
+ The primary implemented command is:
39
39
 
40
40
  ```text
41
- npx architectonic
42
- npx architectonic help
43
- npx architectonic add <layer>
41
+ npx architectonic add teleology
42
+ npx architectonic add identity
43
+ npx architectonic add project
44
+ npx architectonic add skills
45
+ npx architectonic add teleology identity skills
46
+ npx architectonic add skills --dir ./vendor
47
+ npx architectonic add teleology --source npm
48
+ npx architectonic list
49
+ npx architectonic doctor
44
50
  ```
45
51
 
46
- The `add` surface is reserved, but the installer logic is not implemented in
47
- `0.0.2` yet.
52
+ `add` installs from the Architectonic GitHub organization into the current
53
+ directory by default:
54
+
55
+ ```text
56
+ ./teleology
57
+ ./identity
58
+ ./project
59
+ ./skills
60
+ ./architectonic.json
61
+ ```
62
+
63
+ `architectonic.json` records what was installed and where it landed.
64
+
65
+ If a target directory already exists, the command stops instead of silently
66
+ overwriting it.
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.
48
91
 
49
92
  ## Run vs install
50
93
 
@@ -1,12 +1,25 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import { spawnSync } from "node:child_process";
6
+
7
+ const VERSION = "0.0.4";
3
8
  const args = process.argv.slice(2);
4
- const [command, ...targets] = args;
9
+ const [command, ...rest] = args;
5
10
  const supported = ["teleology", "identity", "project", "skills"];
6
11
  const supportedSet = new Set(supported);
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
+ };
7
20
 
8
21
  function printHelp() {
9
- console.log(`architectonic 0.0.2
22
+ console.log(`architectonic ${VERSION}
10
23
 
11
24
  CLI for composing the core layers of an agentic system.
12
25
 
@@ -15,6 +28,10 @@ Usage:
15
28
  npx architectonic help
16
29
  npx architectonic add <teleology|identity|project|skills>
17
30
  npx architectonic add teleology identity skills
31
+ npx architectonic add skills --dir ./vendor
32
+ npx architectonic add teleology --source npm
33
+ npx architectonic list
34
+ npx architectonic doctor
18
35
 
19
36
  Layers:
20
37
  teleology purpose, principles, doctrine, governance
@@ -27,36 +44,394 @@ Run vs install:
27
44
  npm install architectonic install in a project
28
45
  npm install -g architectonic install globally
29
46
 
30
- Status:
31
- 0.0.2 reserves the public CLI name and establishes the command surface.
32
- The add subcommands are placeholders in this initial release.`);
47
+ What add does:
48
+ Installs the selected layer repositories into the target directory
49
+ from git or npm sources and records them in architectonic.json.`);
50
+ }
51
+
52
+ function parseAddArgs(tokens) {
53
+ const targets = [];
54
+ let installDir = process.cwd();
55
+ let source = process.env.ARCHITECTONIC_ADD_SOURCE || "git";
56
+
57
+ for (let index = 0; index < tokens.length; index += 1) {
58
+ const token = tokens[index];
59
+ if (token === "--dir" || token === "--out") {
60
+ const next = tokens[index + 1];
61
+ if (!next) {
62
+ throw new Error(`Missing value for ${token}`);
63
+ }
64
+ installDir = path.resolve(next);
65
+ index += 1;
66
+ continue;
67
+ }
68
+ if (token.startsWith("--dir=")) {
69
+ installDir = path.resolve(token.slice("--dir=".length));
70
+ continue;
71
+ }
72
+ if (token.startsWith("--out=")) {
73
+ installDir = path.resolve(token.slice("--out=".length));
74
+ continue;
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
+ }
89
+ if (token.startsWith("-")) {
90
+ throw new Error(`Unknown option: ${token}`);
91
+ }
92
+ targets.push(token);
93
+ }
94
+
95
+ return { targets, installDir, source };
96
+ }
97
+
98
+ function ensureGitAvailable() {
99
+ const result = spawnSync("git", ["--version"], { encoding: "utf8" });
100
+ if (result.status !== 0) {
101
+ throw new Error("git is required on PATH for `architectonic add`.");
102
+ }
103
+ }
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
+
119
+ function repoUrlFor(target) {
120
+ const normalizedBase = repoBase.replace(/\\/g, "/");
121
+ if (/^(?:[A-Za-z]:\/|\/|\.{1,2}\/)/.test(normalizedBase)) {
122
+ return path.resolve(normalizedBase, target);
123
+ }
124
+ return `${normalizedBase}/${target}.git`;
125
+ }
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
+
144
+ function targetPathFor(installDir, target) {
145
+ return path.join(installDir, target);
146
+ }
147
+
148
+ function manifestPathFor(installDir) {
149
+ return path.join(installDir, "architectonic.json");
150
+ }
151
+
152
+ function readManifest(manifestPath) {
153
+ if (!fs.existsSync(manifestPath)) {
154
+ return {
155
+ schema_version: 1,
156
+ installed_at: new Date().toISOString(),
157
+ layers: {},
158
+ };
159
+ }
160
+
161
+ return JSON.parse(fs.readFileSync(manifestPath, "utf8"));
162
+ }
163
+
164
+ function writeManifest(manifestPath, manifest) {
165
+ fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
166
+ }
167
+
168
+ function cloneLayer(target, installDir) {
169
+ const repoUrl = repoUrlFor(target);
170
+ const targetPath = targetPathFor(installDir, target);
171
+
172
+ if (fs.existsSync(targetPath)) {
173
+ throw new Error(`Target already exists: ${targetPath}`);
174
+ }
175
+
176
+ const clone = spawnSync("git", ["clone", repoUrl, targetPath], {
177
+ cwd: installDir,
178
+ encoding: "utf8",
179
+ stdio: "pipe",
180
+ });
181
+
182
+ if (clone.status !== 0) {
183
+ const detail = (clone.stderr || clone.stdout || "").trim();
184
+ throw new Error(`Failed to clone ${repoUrl}${detail ? `\n${detail}` : ""}`);
185
+ }
186
+
187
+ return {
188
+ name: target,
189
+ repo: repoUrl,
190
+ path: targetPath,
191
+ source: "git",
192
+ };
193
+ }
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
+ }
33
268
  }
34
269
 
35
- if (!command || command === "help" || command === "--help" || command === "-h") {
36
- printHelp();
37
- process.exit(0);
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.`);
38
278
  }
39
279
 
40
- if (command === "add") {
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
+
288
+ function addCommand(tokens) {
289
+ const { targets, installDir, source } = parseAddArgs(tokens);
290
+
41
291
  if (!targets.length) {
42
- console.error("Specify one or more layers: teleology, identity, project, skills");
43
- process.exit(1);
292
+ throw new Error("Specify one or more layers: teleology, identity, project, skills");
44
293
  }
45
294
 
46
295
  const invalid = targets.filter((target) => !supportedSet.has(target));
47
296
  if (invalid.length) {
48
- console.error(`Unknown layer(s): ${invalid.join(", ")}`);
49
- console.error(`Supported layers: ${supported.join(", ")}`);
50
- process.exit(1);
297
+ throw new Error(`Unknown layer(s): ${invalid.join(", ")}\nSupported layers: ${supported.join(", ")}`);
298
+ }
299
+
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
+
309
+ fs.mkdirSync(installDir, { recursive: true });
310
+
311
+ const installed = [];
312
+ for (const target of targets) {
313
+ console.log(`Adding ${target} from ${source}...`);
314
+ installed.push(installLayer(target, installDir, source));
51
315
  }
52
316
 
53
- console.log(`architectonic add ${targets.join(" ")}`);
317
+ const manifestPath = manifestPathFor(installDir);
318
+ const manifest = readManifest(manifestPath);
319
+ manifest.installed_at = new Date().toISOString();
320
+ manifest.last_source = source;
321
+ manifest.git_source_base = repoBase;
322
+ manifest.npm_source_base = npmBase || "registry";
323
+
324
+ 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
+ };
332
+ }
333
+
334
+ writeManifest(manifestPath, manifest);
335
+
54
336
  console.log("");
55
- console.log("This placeholder release does not install packages yet.");
56
- console.log("It reserves the command contract for future cascade/install behavior.");
57
- process.exit(0);
337
+ console.log(`Installed ${installed.map((item) => item.name).join(", ")} into ${installDir}`);
338
+ console.log(`Wrote ${manifestPath}`);
58
339
  }
59
340
 
60
- console.error(`Unknown command: ${command}`);
61
- console.error("Run `architectonic help` for usage.");
62
- process.exit(1);
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
+
412
+ try {
413
+ if (!command || command === "help" || command === "--help" || command === "-h") {
414
+ printHelp();
415
+ process.exit(0);
416
+ }
417
+
418
+ if (command === "add") {
419
+ addCommand(rest);
420
+ process.exit(0);
421
+ }
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
+
433
+ throw new Error(`Unknown command: ${command}\nRun \`architectonic help\` for usage.`);
434
+ } catch (error) {
435
+ console.error(error.message);
436
+ process.exit(1);
437
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "architectonic",
3
- "version": "0.0.2",
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",