architectonic 0.0.3 → 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 +65 -3
- package/bin/architectonic.js +711 -22
- package/package.json +1 -1
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.
|
|
16
|
-
|
|
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`,
|
|
@@ -44,9 +47,18 @@ npx architectonic add project
|
|
|
44
47
|
npx architectonic add skills
|
|
45
48
|
npx architectonic add teleology identity skills
|
|
46
49
|
npx architectonic add skills --dir ./vendor
|
|
50
|
+
npx architectonic add teleology --source npm
|
|
51
|
+
npx architectonic init MyWorkspace
|
|
52
|
+
npx architectonic init --preset company
|
|
53
|
+
npx architectonic list
|
|
54
|
+
npx architectonic doctor
|
|
55
|
+
npx architectonic doctor --fix
|
|
56
|
+
npx architectonic update
|
|
57
|
+
npx architectonic update --dry-run
|
|
58
|
+
npx architectonic remove skills
|
|
47
59
|
```
|
|
48
60
|
|
|
49
|
-
`add`
|
|
61
|
+
`add` installs from the Architectonic GitHub organization into the current
|
|
50
62
|
directory by default:
|
|
51
63
|
|
|
52
64
|
```text
|
|
@@ -62,6 +74,56 @@ directory by default:
|
|
|
62
74
|
If a target directory already exists, the command stops instead of silently
|
|
63
75
|
overwriting it.
|
|
64
76
|
|
|
77
|
+
## Sources
|
|
78
|
+
|
|
79
|
+
`add` supports two source modes:
|
|
80
|
+
|
|
81
|
+
```text
|
|
82
|
+
--source git # clone from GitHub or another git base
|
|
83
|
+
--source npm # pack and extract from npm packages
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The default is `git`.
|
|
87
|
+
|
|
88
|
+
Environment overrides:
|
|
89
|
+
|
|
90
|
+
```text
|
|
91
|
+
ARCHITECTONIC_SOURCE_BASE # override the git source base
|
|
92
|
+
ARCHITECTONIC_NPM_BASE # override the npm package base or local package root
|
|
93
|
+
ARCHITECTONIC_ADD_SOURCE # change the default source mode
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`list` reads `architectonic.json` and shows installed layers.
|
|
97
|
+
|
|
98
|
+
`doctor` verifies that each recorded layer still exists and that the installed
|
|
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.
|
|
126
|
+
|
|
65
127
|
## Run vs install
|
|
66
128
|
|
|
67
129
|
```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.5";
|
|
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,13 @@ 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 init [name]
|
|
34
|
+
npx architectonic list
|
|
35
|
+
npx architectonic doctor
|
|
36
|
+
npx architectonic doctor --fix
|
|
37
|
+
npx architectonic update
|
|
38
|
+
npx architectonic remove <layer>
|
|
25
39
|
|
|
26
40
|
Layers:
|
|
27
41
|
teleology purpose, principles, doctrine, governance
|
|
@@ -35,13 +49,18 @@ Run vs install:
|
|
|
35
49
|
npm install -g architectonic install globally
|
|
36
50
|
|
|
37
51
|
What add does:
|
|
38
|
-
|
|
39
|
-
and records them in architectonic.json.`);
|
|
52
|
+
Installs the selected layer repositories into the target directory
|
|
53
|
+
from git or npm sources and records them in architectonic.json.`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function printUsageError(message) {
|
|
57
|
+
throw new Error(`${message}\nRun \`architectonic help\` for usage.`);
|
|
40
58
|
}
|
|
41
59
|
|
|
42
60
|
function parseAddArgs(tokens) {
|
|
43
61
|
const targets = [];
|
|
44
62
|
let installDir = process.cwd();
|
|
63
|
+
let source = process.env.ARCHITECTONIC_ADD_SOURCE || "git";
|
|
45
64
|
|
|
46
65
|
for (let index = 0; index < tokens.length; index += 1) {
|
|
47
66
|
const token = tokens[index];
|
|
@@ -62,13 +81,173 @@ function parseAddArgs(tokens) {
|
|
|
62
81
|
installDir = path.resolve(token.slice("--out=".length));
|
|
63
82
|
continue;
|
|
64
83
|
}
|
|
84
|
+
if (token === "--source") {
|
|
85
|
+
const next = tokens[index + 1];
|
|
86
|
+
if (!next) {
|
|
87
|
+
throw new Error("Missing value for --source");
|
|
88
|
+
}
|
|
89
|
+
source = next;
|
|
90
|
+
index += 1;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (token.startsWith("--source=")) {
|
|
94
|
+
source = token.slice("--source=".length);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
65
97
|
if (token.startsWith("-")) {
|
|
66
98
|
throw new Error(`Unknown option: ${token}`);
|
|
67
99
|
}
|
|
68
100
|
targets.push(token);
|
|
69
101
|
}
|
|
70
102
|
|
|
71
|
-
return { targets, installDir };
|
|
103
|
+
return { targets, installDir, source };
|
|
104
|
+
}
|
|
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 };
|
|
72
251
|
}
|
|
73
252
|
|
|
74
253
|
function ensureGitAvailable() {
|
|
@@ -78,6 +257,20 @@ function ensureGitAvailable() {
|
|
|
78
257
|
}
|
|
79
258
|
}
|
|
80
259
|
|
|
260
|
+
function ensureNpmAvailable() {
|
|
261
|
+
const result = spawnSync("npm", ["--version"], { encoding: "utf8", shell: true });
|
|
262
|
+
if (result.status !== 0) {
|
|
263
|
+
throw new Error("npm is required on PATH for `architectonic add --source npm`.");
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function ensureTarAvailable() {
|
|
268
|
+
const result = spawnSync("tar", ["--version"], { encoding: "utf8" });
|
|
269
|
+
if (result.status !== 0) {
|
|
270
|
+
throw new Error("tar is required on PATH for npm package extraction.");
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
81
274
|
function repoUrlFor(target) {
|
|
82
275
|
const normalizedBase = repoBase.replace(/\\/g, "/");
|
|
83
276
|
if (/^(?:[A-Za-z]:\/|\/|\.{1,2}\/)/.test(normalizedBase)) {
|
|
@@ -86,10 +279,31 @@ function repoUrlFor(target) {
|
|
|
86
279
|
return `${normalizedBase}/${target}.git`;
|
|
87
280
|
}
|
|
88
281
|
|
|
282
|
+
function packageSpecFor(target) {
|
|
283
|
+
const packageName = packageMap[target];
|
|
284
|
+
if (!packageName) {
|
|
285
|
+
throw new Error(`No npm package mapping defined for ${target}`);
|
|
286
|
+
}
|
|
287
|
+
if (!npmBase) {
|
|
288
|
+
return packageName;
|
|
289
|
+
}
|
|
290
|
+
const normalizedBase = npmBase.replace(/\\/g, "/");
|
|
291
|
+
if (/^(?:[A-Za-z]:\/|\/|\.{1,2}\/)/.test(normalizedBase)) {
|
|
292
|
+
return path.resolve(normalizedBase, target);
|
|
293
|
+
}
|
|
294
|
+
return normalizedBase.endsWith("/")
|
|
295
|
+
? `${normalizedBase}${packageName}`
|
|
296
|
+
: `${normalizedBase}/${packageName}`;
|
|
297
|
+
}
|
|
298
|
+
|
|
89
299
|
function targetPathFor(installDir, target) {
|
|
90
300
|
return path.join(installDir, target);
|
|
91
301
|
}
|
|
92
302
|
|
|
303
|
+
function manifestPathFor(installDir) {
|
|
304
|
+
return path.join(installDir, "architectonic.json");
|
|
305
|
+
}
|
|
306
|
+
|
|
93
307
|
function readManifest(manifestPath) {
|
|
94
308
|
if (!fs.existsSync(manifestPath)) {
|
|
95
309
|
return {
|
|
@@ -106,6 +320,14 @@ function writeManifest(manifestPath, manifest) {
|
|
|
106
320
|
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
|
|
107
321
|
}
|
|
108
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
|
+
|
|
109
331
|
function cloneLayer(target, installDir) {
|
|
110
332
|
const repoUrl = repoUrlFor(target);
|
|
111
333
|
const targetPath = targetPathFor(installDir, target);
|
|
@@ -129,41 +351,171 @@ function cloneLayer(target, installDir) {
|
|
|
129
351
|
name: target,
|
|
130
352
|
repo: repoUrl,
|
|
131
353
|
path: targetPath,
|
|
354
|
+
source: "git",
|
|
132
355
|
};
|
|
133
356
|
}
|
|
134
357
|
|
|
135
|
-
function
|
|
136
|
-
const
|
|
358
|
+
function packNpmLayer(target, installDir) {
|
|
359
|
+
const packageSpec = packageSpecFor(target);
|
|
360
|
+
const targetPath = targetPathFor(installDir, target);
|
|
361
|
+
const tempRoot = fs.mkdtempSync(path.join(installDir, "architectonic-npm-"));
|
|
362
|
+
const pack = spawnSync("npm", ["pack", packageSpec, "--pack-destination", tempRoot], {
|
|
363
|
+
cwd: installDir,
|
|
364
|
+
encoding: "utf8",
|
|
365
|
+
stdio: "pipe",
|
|
366
|
+
shell: true,
|
|
367
|
+
});
|
|
137
368
|
|
|
138
|
-
if (
|
|
139
|
-
|
|
369
|
+
if (pack.status !== 0) {
|
|
370
|
+
const detail = (pack.stderr || pack.stdout || "").trim();
|
|
371
|
+
throw new Error(`Failed to pack ${packageSpec}${detail ? `\n${detail}` : ""}`);
|
|
140
372
|
}
|
|
141
373
|
|
|
142
|
-
const
|
|
143
|
-
if (
|
|
144
|
-
throw new Error(`
|
|
374
|
+
const tgzName = (pack.stdout || "").trim().split(/\r?\n/).filter(Boolean).pop();
|
|
375
|
+
if (!tgzName) {
|
|
376
|
+
throw new Error(`npm pack did not return a tarball name for ${packageSpec}`);
|
|
145
377
|
}
|
|
146
378
|
|
|
147
|
-
|
|
379
|
+
const tgzPath = path.join(tempRoot, tgzName);
|
|
380
|
+
if (!fs.existsSync(tgzPath)) {
|
|
381
|
+
throw new Error(`Packed tarball not found: ${tgzPath}`);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (fs.existsSync(targetPath)) {
|
|
385
|
+
throw new Error(`Target already exists: ${targetPath}`);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
fs.mkdirSync(targetPath, { recursive: true });
|
|
389
|
+
const extract = spawnSync("tar", ["-xzf", tgzPath, "-C", targetPath], {
|
|
390
|
+
cwd: installDir,
|
|
391
|
+
encoding: "utf8",
|
|
392
|
+
stdio: "pipe",
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
if (extract.status !== 0) {
|
|
396
|
+
const detail = (extract.stderr || extract.stdout || "").trim();
|
|
397
|
+
throw new Error(`Failed to extract ${tgzPath}${detail ? `\n${detail}` : ""}`);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const extractedPackageDir = path.join(targetPath, "package");
|
|
401
|
+
if (!fs.existsSync(extractedPackageDir)) {
|
|
402
|
+
throw new Error(`Expected extracted package directory at ${extractedPackageDir}`);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
for (const entry of fs.readdirSync(extractedPackageDir, { withFileTypes: true })) {
|
|
406
|
+
const from = path.join(extractedPackageDir, entry.name);
|
|
407
|
+
const to = path.join(targetPath, entry.name);
|
|
408
|
+
fs.renameSync(from, to);
|
|
409
|
+
}
|
|
410
|
+
fs.rmdirSync(extractedPackageDir);
|
|
411
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
412
|
+
|
|
413
|
+
return {
|
|
414
|
+
name: target,
|
|
415
|
+
repo: packageSpec,
|
|
416
|
+
path: targetPath,
|
|
417
|
+
source: "npm",
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function readInstalledPackageName(layerPath) {
|
|
422
|
+
const packageJsonPath = path.join(layerPath, "package.json");
|
|
423
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
424
|
+
return null;
|
|
425
|
+
}
|
|
426
|
+
try {
|
|
427
|
+
return readJson(packageJsonPath).name || null;
|
|
428
|
+
} catch {
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function readInstalledVersion(layerPath) {
|
|
434
|
+
const packageJsonPath = path.join(layerPath, "package.json");
|
|
435
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
436
|
+
return null;
|
|
437
|
+
}
|
|
438
|
+
try {
|
|
439
|
+
return readJson(packageJsonPath).version || null;
|
|
440
|
+
} catch {
|
|
441
|
+
return null;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function hasGitRepo(layerPath) {
|
|
446
|
+
return pathExists(path.join(layerPath, ".git"));
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function gitResult(args, cwd) {
|
|
450
|
+
return spawnSync("git", args, {
|
|
451
|
+
cwd,
|
|
452
|
+
encoding: "utf8",
|
|
453
|
+
stdio: "pipe",
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function isGitWorktreeDirty(layerPath) {
|
|
458
|
+
const result = gitResult(["status", "--porcelain"], layerPath);
|
|
459
|
+
if (result.status !== 0) {
|
|
460
|
+
return null;
|
|
461
|
+
}
|
|
462
|
+
return Boolean((result.stdout || "").trim());
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function packageMetaFor(target, layerPath) {
|
|
466
|
+
return {
|
|
467
|
+
expectedPackageName: packageMap[target] || null,
|
|
468
|
+
packageName: readInstalledPackageName(layerPath),
|
|
469
|
+
version: readInstalledVersion(layerPath),
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function relativeManifestPath(installDir, absolutePath) {
|
|
474
|
+
return `./${path.relative(installDir, absolutePath).replace(/\\/g, "/")}`;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function ensureInstallPrereqs(source) {
|
|
478
|
+
if (source === "git") {
|
|
479
|
+
ensureGitAvailable();
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
if (source === "npm") {
|
|
483
|
+
ensureNpmAvailable();
|
|
484
|
+
ensureTarAvailable();
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
throw new Error(`Unsupported source: ${source}. Use git or npm.`);
|
|
488
|
+
}
|
|
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);
|
|
148
502
|
fs.mkdirSync(installDir, { recursive: true });
|
|
149
503
|
|
|
150
504
|
const installed = [];
|
|
151
505
|
for (const target of targets) {
|
|
152
|
-
console.log(`Adding ${target}...`);
|
|
153
|
-
installed.push(
|
|
506
|
+
console.log(`Adding ${target} from ${source}...`);
|
|
507
|
+
installed.push(installLayer(target, installDir, source));
|
|
154
508
|
}
|
|
155
509
|
|
|
156
|
-
const manifestPath =
|
|
510
|
+
const manifestPath = manifestPathFor(installDir);
|
|
157
511
|
const manifest = readManifest(manifestPath);
|
|
158
512
|
manifest.installed_at = new Date().toISOString();
|
|
159
|
-
manifest.
|
|
513
|
+
manifest.last_source = source;
|
|
514
|
+
manifest.git_source_base = repoBase;
|
|
515
|
+
manifest.npm_source_base = npmBase || "registry";
|
|
160
516
|
|
|
161
517
|
for (const item of installed) {
|
|
162
|
-
manifest
|
|
163
|
-
repo: item.repo,
|
|
164
|
-
path: `./${path.relative(installDir, item.path).replace(/\\/g, "/")}`,
|
|
165
|
-
installed_at: manifest.installed_at,
|
|
166
|
-
};
|
|
518
|
+
updateManifestLayerRecord(manifest, installDir, item.name, item.path, item.source, item.repo);
|
|
167
519
|
}
|
|
168
520
|
|
|
169
521
|
writeManifest(manifestPath, manifest);
|
|
@@ -173,18 +525,355 @@ function addCommand(tokens) {
|
|
|
173
525
|
console.log(`Wrote ${manifestPath}`);
|
|
174
526
|
}
|
|
175
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
|
+
|
|
608
|
+
function listCommand(tokens) {
|
|
609
|
+
const installDir = tokens[0] ? path.resolve(tokens[0]) : process.cwd();
|
|
610
|
+
const { manifest } = loadManifestFromDir(installDir);
|
|
611
|
+
const entries = Object.entries(manifest.layers || {});
|
|
612
|
+
|
|
613
|
+
if (!entries.length) {
|
|
614
|
+
console.log("No layers installed.");
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
for (const [name, layer] of entries) {
|
|
619
|
+
console.log(`${name}`);
|
|
620
|
+
console.log(` source: ${layer.source || "unknown"}`);
|
|
621
|
+
console.log(` path: ${layer.path || "unknown"}`);
|
|
622
|
+
console.log(` ref: ${layer.ref || "unknown"}`);
|
|
623
|
+
if (layer.package_name) {
|
|
624
|
+
console.log(` pkg: ${layer.package_name}`);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function inferLayerPath(installDir, layerName) {
|
|
630
|
+
const candidate = targetPathFor(installDir, layerName);
|
|
631
|
+
return pathExists(candidate) ? candidate : null;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function doctorCommand(tokens) {
|
|
635
|
+
const { installDir, fix } = parseDoctorArgs(tokens);
|
|
636
|
+
const { manifestPath, manifest } = loadManifestFromDir(installDir);
|
|
637
|
+
const entries = Object.entries(manifest.layers || {});
|
|
638
|
+
let failures = 0;
|
|
639
|
+
let changed = false;
|
|
640
|
+
|
|
641
|
+
console.log(`architectonic doctor`);
|
|
642
|
+
console.log(` root: ${installDir}`);
|
|
643
|
+
console.log(` manifest: ${manifestPath}`);
|
|
644
|
+
|
|
645
|
+
if (!entries.length) {
|
|
646
|
+
console.log(` [fail] no installed layers recorded`);
|
|
647
|
+
process.exit(1);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
for (const [name, layer] of entries) {
|
|
651
|
+
const relativePath = String(layer.path || "");
|
|
652
|
+
const layerPath = path.resolve(installDir, relativePath);
|
|
653
|
+
const packageJsonPath = path.join(layerPath, "package.json");
|
|
654
|
+
const exists = fs.existsSync(layerPath);
|
|
655
|
+
const hasPackageJson = fs.existsSync(packageJsonPath);
|
|
656
|
+
const packageName = hasPackageJson ? readInstalledPackageName(layerPath) : null;
|
|
657
|
+
const expectedPackageName = packageMap[name];
|
|
658
|
+
|
|
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
|
+
}
|
|
667
|
+
console.log(` [fail] ${name}: missing directory ${layerPath}`);
|
|
668
|
+
failures += 1;
|
|
669
|
+
continue;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
if (!hasPackageJson) {
|
|
673
|
+
console.log(` [fail] ${name}: missing package.json`);
|
|
674
|
+
failures += 1;
|
|
675
|
+
continue;
|
|
676
|
+
}
|
|
677
|
+
|
|
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
|
+
}
|
|
686
|
+
console.log(` [fail] ${name}: expected package ${expectedPackageName}, found ${packageName}`);
|
|
687
|
+
failures += 1;
|
|
688
|
+
continue;
|
|
689
|
+
}
|
|
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
|
+
|
|
700
|
+
console.log(` [ok] ${name}: ${relativePath} (${layer.source || "unknown"})`);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
if (fix && changed) {
|
|
704
|
+
writeManifest(manifestPath, manifest);
|
|
705
|
+
console.log(` [write] updated manifest repairs`);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
if (failures > 0) {
|
|
709
|
+
process.exit(1);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
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
|
+
|
|
176
840
|
try {
|
|
177
841
|
if (!command || command === "help" || command === "--help" || command === "-h") {
|
|
178
842
|
printHelp();
|
|
179
843
|
process.exit(0);
|
|
180
844
|
}
|
|
181
845
|
|
|
846
|
+
if (command === "init") {
|
|
847
|
+
initCommand(rest);
|
|
848
|
+
process.exit(0);
|
|
849
|
+
}
|
|
850
|
+
|
|
182
851
|
if (command === "add") {
|
|
183
852
|
addCommand(rest);
|
|
184
853
|
process.exit(0);
|
|
185
854
|
}
|
|
186
855
|
|
|
187
|
-
|
|
856
|
+
if (command === "list") {
|
|
857
|
+
listCommand(rest);
|
|
858
|
+
process.exit(0);
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
if (command === "doctor") {
|
|
862
|
+
doctorCommand(rest);
|
|
863
|
+
process.exit(0);
|
|
864
|
+
}
|
|
865
|
+
|
|
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}`);
|
|
188
877
|
} catch (error) {
|
|
189
878
|
console.error(error.message);
|
|
190
879
|
process.exit(1);
|