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 +30 -3
- package/bin/architectonic.js +257 -11
- package/package.json +1 -1
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.
|
|
16
|
-
|
|
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`
|
|
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
|
package/bin/architectonic.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
313
|
+
console.log(`Adding ${target} from ${source}...`);
|
|
314
|
+
installed.push(installLayer(target, installDir, source));
|
|
154
315
|
}
|
|
155
316
|
|
|
156
|
-
const manifestPath =
|
|
317
|
+
const manifestPath = manifestPathFor(installDir);
|
|
157
318
|
const manifest = readManifest(manifestPath);
|
|
158
319
|
manifest.installed_at = new Date().toISOString();
|
|
159
|
-
manifest.
|
|
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
|
-
|
|
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);
|