@gregiteen/ssss-cli 0.7.0

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.
Files changed (43) hide show
  1. package/CHANGELOG.md +124 -0
  2. package/LICENSE +15 -0
  3. package/README.md +162 -0
  4. package/VERSION +1 -0
  5. package/conformance/README.md +72 -0
  6. package/conformance/fixtures.json +763 -0
  7. package/conformance/reference-bundle.ucw.json +135 -0
  8. package/docs/help/autolink.md +15 -0
  9. package/docs/help/bundle.md +47 -0
  10. package/docs/help/conformance.md +34 -0
  11. package/docs/help/export.md +20 -0
  12. package/docs/help/import.md +19 -0
  13. package/docs/help/overview.md +33 -0
  14. package/docs/help/portability.md +27 -0
  15. package/docs/help/provision.md +19 -0
  16. package/docs/help/provisioning.md +43 -0
  17. package/docs/help/roles.md +61 -0
  18. package/docs/help/runtime.md +78 -0
  19. package/docs/help/scaffold.md +47 -0
  20. package/docs/ssss-spec.md +1325 -0
  21. package/package.json +62 -0
  22. package/registry/core.json +225 -0
  23. package/registry/extensions/festech.json +134 -0
  24. package/scripts/audit-registry-field-usage.mjs +136 -0
  25. package/scripts/autolink.mjs +341 -0
  26. package/scripts/build-reference-bundle.mjs +169 -0
  27. package/scripts/cmd-export.mjs +72 -0
  28. package/scripts/cmd-help.mjs +60 -0
  29. package/scripts/cmd-import.mjs +71 -0
  30. package/scripts/cmd-inspect.mjs +78 -0
  31. package/scripts/cmd-new.mjs +262 -0
  32. package/scripts/cmd-provision.mjs +68 -0
  33. package/scripts/cmd-validate.mjs +43 -0
  34. package/scripts/conformance.mjs +408 -0
  35. package/scripts/lib/cli.mjs +92 -0
  36. package/scripts/release.sh +35 -0
  37. package/scripts/ssss.mjs +93 -0
  38. package/scripts/validate-skills.mjs +115 -0
  39. package/src/bundle.mjs +261 -0
  40. package/src/engine.mjs +452 -0
  41. package/src/frontmatter.mjs +109 -0
  42. package/src/registry.mjs +67 -0
  43. package/src/runtime.mjs +212 -0
@@ -0,0 +1,71 @@
1
+ /**
2
+ * `ssss import` — replay a bundle (or a pre-built plan) into a target vault via
3
+ * the reference engine (spec §17.1). Idempotent: each envelope's idempotency key
4
+ * makes a re-run commit nothing new. Provisions inline unless `--plan` is given.
5
+ */
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+ import { provisionBundle, importBundle } from '../src/bundle.mjs';
9
+ import { createEngine } from '../src/engine.mjs';
10
+ import { parseArgs, parseKeyValues, usage, die, wantsHelp } from './lib/cli.mjs';
11
+
12
+ const HELP = usage({
13
+ summary: 'Replay a bundle/plan into a target vault via the reference engine (§17).',
14
+ usage: 'ssss import <bundle.ucw.json> --vault <dir> [options]',
15
+ options: [
16
+ ['--vault <dir>', 'Target vault directory (created if missing). Required.'],
17
+ ['--plan <file>', 'Import a pre-built plan (from `ssss provision`) instead of the bundle.'],
18
+ ['--param <k=v>', 'Bind a bundle parameter (repeatable).'],
19
+ ['--workspace <id>', 'Target workspace id (default: ws-provision).'],
20
+ ['--prefix <path>', 'Place every file under this path prefix (id-remap).'],
21
+ ['--dry-run', 'Validate every envelope without committing.'],
22
+ ['--registry <dir>', 'Registry directory (default: the package core registry).'],
23
+ ],
24
+ examples: [
25
+ 'ssss import festival.ucw.json --vault ./new-tenant --param domain=acme.live',
26
+ 'ssss provision festival.ucw.json --out plan.json && ssss import --plan plan.json --vault ./t',
27
+ ],
28
+ seeAlso: ['ssss provision', 'ssss export', 'ssss help provisioning'],
29
+ });
30
+
31
+ export async function run(argv) {
32
+ if (wantsHelp(argv)) { console.log(HELP); return; }
33
+ const { positionals, flags } = parseArgs(argv, { booleans: ['dry-run'], multi: ['param'] });
34
+ if (!flags.vault) die('import needs --vault <dir>.\n\n' + HELP);
35
+ const vault = path.resolve(flags.vault);
36
+ fs.mkdirSync(vault, { recursive: true });
37
+
38
+ let envelopes;
39
+ if (flags.plan) {
40
+ if (!fs.existsSync(flags.plan)) die(`no such plan: ${flags.plan}`);
41
+ const plan = JSON.parse(fs.readFileSync(flags.plan, 'utf8'));
42
+ envelopes = plan.envelopes || plan;
43
+ } else {
44
+ const file = positionals[0];
45
+ if (!file) die('import needs a bundle file (or --plan).\n\n' + HELP);
46
+ if (!fs.existsSync(file)) die(`no such file: ${file}`);
47
+ const bundle = JSON.parse(fs.readFileSync(file, 'utf8'));
48
+ const prov = provisionBundle(bundle, {
49
+ parameters: parseKeyValues(flags.param),
50
+ workspaceId: flags.workspace || 'ws-provision',
51
+ pathPrefix: flags.prefix || '',
52
+ dryRun: !!flags['dry-run'],
53
+ });
54
+ if (!prov.ok) {
55
+ if (prov.unresolved.length) console.error(`✗ unresolved parameter(s): ${prov.unresolved.join(', ')}`);
56
+ for (const d of prov.danglingLinks) console.error(`✗ dangling link: ${d.file} → [[${d.ref}]]`);
57
+ for (const e of prov.errors || []) console.error(`✗ bundle validation: ${e}`);
58
+ process.exit(1);
59
+ }
60
+ envelopes = prov.plan;
61
+ }
62
+
63
+ const engine = createEngine({ registryDir: flags.registry });
64
+ const result = importBundle(envelopes, vault, engine);
65
+ const failed = result.results.filter((r) => !r.success);
66
+ console.log(`${result.ok ? '✓' : '✗'} import — ${result.committed} committed, ${envelopes.length - result.committed} unchanged (idempotent), ${failed.length} failed → ${vault}`);
67
+ if (failed.length) {
68
+ for (const r of failed) console.error(` ✗ ${r.path}: ${(r.validation?.errors || []).join('; ')}`);
69
+ process.exit(1);
70
+ }
71
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * `ssss inspect` — human-readable summary of a `.ucw` bundle: manifest,
3
+ * primitive inventory, parameters, provisioning steps, and file list. Read-only.
4
+ */
5
+ import fs from 'node:fs';
6
+ import { validateBundle } from '../src/bundle.mjs';
7
+ import { parseArgs, usage, die, wantsHelp } from './lib/cli.mjs';
8
+
9
+ const HELP = usage({
10
+ summary: 'Print a human-readable summary of a .ucw bundle.',
11
+ usage: 'ssss inspect <bundle.ucw.json> [options]',
12
+ options: [
13
+ ['--files', 'Also list every file path in the bundle.'],
14
+ ['--registry <dir>', 'Registry directory used for the validity check.'],
15
+ ],
16
+ examples: ['ssss inspect festival.ucw.json --files'],
17
+ seeAlso: ['ssss validate', 'ssss provision'],
18
+ });
19
+
20
+ export async function run(argv) {
21
+ if (wantsHelp(argv)) { console.log(HELP); return; }
22
+ const { positionals, flags } = parseArgs(argv, { booleans: ['files'] });
23
+ const file = positionals[0];
24
+ if (!file) die('inspect needs a bundle file.\n\n' + HELP);
25
+ if (!fs.existsSync(file)) die(`no such file: ${file}`);
26
+
27
+ let bundle;
28
+ try { bundle = JSON.parse(fs.readFileSync(file, 'utf8')); }
29
+ catch (err) { die(`not valid JSON: ${err.message}`); }
30
+ const m = bundle.manifest || {};
31
+ const { valid, errors } = validateBundle(bundle, { registryDir: flags.registry });
32
+
33
+ const L = [];
34
+ L.push(`${m.name || '(unnamed)'} — v${m.version || '?'}`);
35
+ if (m.description) L.push(` ${m.description}`);
36
+ L.push('');
37
+ L.push(` profile ${m.export_profile}`);
38
+ L.push(` core version ${m.ssss_core_version}`);
39
+ L.push(` required ext ${(m.required_extensions || []).join(', ') || '(none)'}`);
40
+ L.push(` files ${m.file_count}`);
41
+ L.push(` exported at ${m.exported_at}`);
42
+ L.push(` exporter ${m.provenance?.exporter || '?'}`);
43
+ L.push(` content hash ${m.provenance?.content_hash || '?'}`);
44
+ L.push(` integrity ${valid ? '✓ valid' : '✗ ' + errors.length + ' error(s)'}`);
45
+
46
+ const inv = m.primitive_inventory || {};
47
+ const invKeys = Object.keys(inv).sort();
48
+ if (invKeys.length) {
49
+ L.push('', ' Primitive inventory:');
50
+ for (const k of invKeys) L.push(` ${String(inv[k]).padStart(4)} ${k}`);
51
+ }
52
+
53
+ if ((m.parameters || []).length) {
54
+ L.push('', ' Parameters:');
55
+ for (const p of m.parameters) {
56
+ L.push(` ${p.key} (${p.type}, ${p.source}${p.required ? ', required' : ''}) ${p.label || ''}`);
57
+ }
58
+ }
59
+
60
+ if ((m.provisioning || []).length) {
61
+ L.push('', ' Provisioning steps:');
62
+ for (const s of m.provisioning) {
63
+ L.push(` [${s.required ? '!' : ' '}] ${s.id} ${s.system}/${s.mode} ${s.label || ''}`);
64
+ }
65
+ }
66
+
67
+ if (flags.files) {
68
+ L.push('', ' Files:');
69
+ for (const f of bundle.files || []) L.push(` ${f.path}`);
70
+ }
71
+
72
+ console.log(L.join('\n'));
73
+ if (!valid) {
74
+ console.error('\nValidation errors:');
75
+ for (const e of errors) console.error(` - ${e}`);
76
+ process.exit(1);
77
+ }
78
+ }
@@ -0,0 +1,262 @@
1
+ /**
2
+ * `ssss new` — scaffold a fresh repo wired for SSSS (and, optionally, Total Recall).
3
+ *
4
+ * Produces a ready-to-run project: a starter `vault/` of core primitives, the
5
+ * `@ssss/cli` dependency pinned to this CLI's own version tag, a dependency-free
6
+ * conformance test (`node --test`) that replays the canonical fixtures through the
7
+ * engine AND round-trips the vault as a `sale` bundle, plus a CLAUDE.md describing
8
+ * the conventions. With `--with-total-recall` it also wires the Total Recall
9
+ * memory OS. Dependency-free; uses only Node built-ins.
10
+ *
11
+ * All shelling-out uses execFileSync (no shell) with fixed argument vectors —
12
+ * never string interpolation — so there is no command-injection surface.
13
+ */
14
+ import fs from 'node:fs';
15
+ import path from 'node:path';
16
+ import { execFileSync } from 'node:child_process';
17
+ import { fileURLToPath } from 'node:url';
18
+ import { parseArgs, usage, die, wantsHelp } from './lib/cli.mjs';
19
+
20
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
21
+ const VERSION = (() => {
22
+ try { return fs.readFileSync(path.resolve(HERE, '..', 'VERSION'), 'utf8').trim(); }
23
+ catch { return '0.0.0'; }
24
+ })();
25
+
26
+ const HELP = usage({
27
+ summary: 'Scaffold a new repo wired for SSSS (and optionally Total Recall).',
28
+ usage: 'ssss new <dir> [options]',
29
+ options: [
30
+ ['--name <s>', 'Package name (default: the target directory name).'],
31
+ ['--with-total-recall', 'Also wire the Total Recall memory OS (total-recall-brain).'],
32
+ ['--install', 'Run `npm install` after scaffolding (and Total Recall init if requested).'],
33
+ ['--no-git', 'Do not run `git init`.'],
34
+ ['--ref <git-ref>', `Pin @ssss/cli to this ref (default: v${VERSION}).`],
35
+ ['--force', 'Allow scaffolding into a non-empty directory.'],
36
+ ],
37
+ examples: [
38
+ 'ssss new my-app',
39
+ 'ssss new my-app --with-total-recall --install',
40
+ ],
41
+ seeAlso: ['ssss help scaffold', 'ssss export', 'ssss conformance'],
42
+ });
43
+
44
+ const W = (root, rel, content) => {
45
+ const abs = path.join(root, rel);
46
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
47
+ fs.writeFileSync(abs, content);
48
+ };
49
+
50
+ /** Run a fixed command with no shell; returns true on success. */
51
+ function tryRun(file, args, cwd, stdio = 'inherit') {
52
+ try { execFileSync(file, args, { cwd, stdio }); return true; }
53
+ catch { return false; }
54
+ }
55
+
56
+ export async function run(argv) {
57
+ if (wantsHelp(argv)) { console.log(HELP); return; }
58
+ const { positionals, flags } = parseArgs(argv, {
59
+ booleans: ['with-total-recall', 'install', 'no-git', 'force'],
60
+ });
61
+ const target = positionals[0];
62
+ if (!target) die('new needs a target directory.\n\n' + HELP);
63
+
64
+ const root = path.resolve(target);
65
+ const name = flags.name || path.basename(root);
66
+ const ref = flags.ref || `v${VERSION}`;
67
+ const withTR = !!flags['with-total-recall'];
68
+
69
+ if (fs.existsSync(root) && fs.readdirSync(root).length && !flags.force) {
70
+ die(`${root} is not empty. Use --force to scaffold anyway.`);
71
+ }
72
+ fs.mkdirSync(root, { recursive: true });
73
+
74
+ // ── package.json ────────────────────────────────────────────────────────
75
+ const pkg = {
76
+ name, version: '0.1.0', private: true, type: 'module',
77
+ description: 'An SSSS-native project — Markdown vault as source of truth.',
78
+ scripts: {
79
+ ssss: 'ssss',
80
+ test: 'node --test',
81
+ export: 'ssss export vault --profile sale --out dist/bundle.ucw.json',
82
+ validate: 'ssss conformance --engine',
83
+ },
84
+ dependencies: { '@ssss/cli': `github:gregiteen/ssss#${ref}` },
85
+ ...(withTR ? { devDependencies: { 'total-recall-brain': 'latest' } } : {}),
86
+ };
87
+ W(root, 'package.json', JSON.stringify(pkg, null, 2) + '\n');
88
+
89
+ // ── starter vault (core primitives; the task is tenant_private) ──────────
90
+ W(root, 'vault/rules/welcome.md',
91
+ '---\ntype: rule\ntitle: Welcome Rule\ndescription: Starter structural rule for an SSSS-native app.\ntimestamp: 2026-07-02T00:00:00Z\nname: Welcome Rule\n---\n\n# Welcome\n\nA starter **structural** rule — the kind of thing that ships in a `sale` bundle.\n');
92
+ W(root, 'vault/workflows/onboarding.md',
93
+ '---\ntype: workflow\ntitle: Onboarding\ndescription: Starter workflow that applies the welcome rule and greets the operator.\ntimestamp: 2026-07-02T00:00:00Z\nname: Onboarding\n---\n\n# Onboarding\n\n1. Apply the [[welcome]] rule.\n2. Greet the operator.\n');
94
+ W(root, 'vault/assistants/concierge.md',
95
+ '---\ntype: assistant\ntitle: Concierge\ndescription: Starter assistant definition for the project.\ntimestamp: 2026-07-02T00:00:00Z\nname: Concierge\n---\n\n# Concierge\n\nA starter assistant definition.\n');
96
+ W(root, 'vault/tasks/first-task.md',
97
+ '---\ntype: task\ntitle: First Task\ndescription: Starter private setup task that demonstrates tenant_private filtering.\ntimestamp: 2026-07-02T00:00:00Z\npriority: normal\ncategory: setup\nstatus: open\n---\n\n# First task\n\nThis is `tenant_private` (§5.5) — it is **dropped** from `template`/`sale` exports.\n');
98
+
99
+ // ── conformance test (zero-dependency: node --test) ─────────────────────
100
+ W(root, 'test/ssss-conformance.test.mjs', CONFORMANCE_TEST);
101
+
102
+ // ── repo meta ───────────────────────────────────────────────────────────
103
+ W(root, '.gitignore', 'node_modules/\ndist/\n*.ucw.json\n.DS_Store\n');
104
+ W(root, 'README.md', readme(name, withTR));
105
+ W(root, 'CLAUDE.md', claudeMd(name, withTR));
106
+
107
+ // ── git + install (best-effort, flag-gated, no shell) ───────────────────
108
+ const log = (m) => console.error(m);
109
+ log(`Scaffolded SSSS project → ${root}`);
110
+ log(` @ssss/cli pinned to github:gregiteen/ssss#${ref}`);
111
+ if (withTR) log(' Total Recall wired (total-recall-brain).');
112
+
113
+ if (!flags['no-git'] && !fs.existsSync(path.join(root, '.git'))) {
114
+ if (tryRun('git', ['init', '-q'], root, 'ignore')) log(' git initialized.');
115
+ }
116
+
117
+ if (flags.install) {
118
+ log('\nInstalling dependencies (npm install)…');
119
+ if (!tryRun('npm', ['install', '--no-audit', '--no-fund'], root)) {
120
+ die('scaffolded, but `npm install` failed. Finish manually with the steps below.', 1);
121
+ }
122
+ if (withTR) {
123
+ log('\nInitializing Total Recall memory vault…');
124
+ tryRun('npx', ['-p', 'total-recall-brain', 'total-recall', 'init'], root);
125
+ }
126
+ log('\nVerifying the toolchain (npm test)…');
127
+ if (!tryRun('npm', ['test'], root)) {
128
+ die('scaffolded and installed, but `npm test` failed. Inspect the output above.', 1);
129
+ }
130
+ }
131
+
132
+ // ── next steps ──────────────────────────────────────────────────────────
133
+ console.log(`\n✓ ${name} is ready.\n`);
134
+ console.log('Next steps:');
135
+ console.log(` cd ${path.relative(process.cwd(), root) || '.'}`);
136
+ if (!flags.install) {
137
+ console.log(' npm install');
138
+ if (withTR) console.log(' npx -p total-recall-brain total-recall init # seed the memory vault');
139
+ }
140
+ console.log(' npm test # replays the canonical fixtures + round-trips the vault');
141
+ console.log(' npx ssss export vault --profile sale --out dist/bundle.ucw.json');
142
+ console.log(' npx ssss help # explore the standard');
143
+ }
144
+
145
+ const CONFORMANCE_TEST = `/**
146
+ * SSSS conformance — proves this repo's toolchain matches the published standard.
147
+ * Zero test dependencies: run with \`node --test\` (Node 18+).
148
+ */
149
+ import { test } from 'node:test';
150
+ import assert from 'node:assert/strict';
151
+ import { createRequire } from 'node:module';
152
+ import fs from 'node:fs';
153
+ import os from 'node:os';
154
+ import path from 'node:path';
155
+ import { fileURLToPath } from 'node:url';
156
+ import { createEngine } from '@ssss/cli/engine';
157
+ import { exportBundle, validateBundle, provisionBundle, importBundle } from '@ssss/cli/bundle';
158
+
159
+ const require = createRequire(import.meta.url);
160
+ const { fixtures } = require('@ssss/cli/conformance/fixtures.json');
161
+ const VAULT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'vault');
162
+
163
+ test('engine implements the canonical Operation Contract (spec §6)', () => {
164
+ const vault = fs.mkdtempSync(path.join(os.tmpdir(), 'ssss-conf-'));
165
+ const engine = createEngine();
166
+ try {
167
+ for (const f of fixtures) {
168
+ const res = engine.processOperation(JSON.parse(JSON.stringify(f.request)), vault);
169
+ const exp = f.expected_response;
170
+ if (exp.success !== undefined) assert.equal(res.success, exp.success, f.id + ' success');
171
+ if (exp.validation && exp.validation.valid !== undefined) {
172
+ assert.equal(res.validation && res.validation.valid, exp.validation.valid, f.id + ' valid');
173
+ }
174
+ }
175
+ } finally { fs.rmSync(vault, { recursive: true, force: true }); }
176
+ });
177
+
178
+ test('this vault exports, validates, and round-trips as a sale bundle (spec §16/§17)', () => {
179
+ const bundle = exportBundle(VAULT, { profile: 'sale', name: 'starter' });
180
+ const { valid, errors } = validateBundle(bundle);
181
+ assert.ok(valid, 'bundle invalid: ' + errors.join('; '));
182
+ assert.ok(!bundle.files.some((f) => f.path.startsWith('tasks/')), 'tenant_private leaked into sale export');
183
+
184
+ const plan = provisionBundle(bundle, { workspaceId: 'ws-test' });
185
+ assert.ok(plan.ok, 'provision failed: ' + JSON.stringify(plan.unresolved) + ' ' + JSON.stringify(plan.danglingLinks));
186
+
187
+ const target = fs.mkdtempSync(path.join(os.tmpdir(), 'ssss-import-'));
188
+ try {
189
+ const engine = createEngine();
190
+ const r1 = importBundle(plan.plan, target, engine);
191
+ assert.ok(r1.ok && r1.committed === bundle.files.length, 'import did not commit every file');
192
+ const r2 = importBundle(plan.plan, target, engine);
193
+ assert.equal(r2.committed, 0, 're-import was not idempotent');
194
+ } finally { fs.rmSync(target, { recursive: true, force: true }); }
195
+ });
196
+ `;
197
+
198
+ function readme(name, withTR) {
199
+ return `# ${name}
200
+
201
+ An [SSSS](https://github.com/gregiteen/ssss)-native project: a Markdown **vault** is
202
+ the source of truth, validated and packaged by the dependency-free \`@ssss/cli\`.
203
+
204
+ ## Layout
205
+
206
+ \`\`\`
207
+ vault/ Your SSSS documents (structural + tenant_private primitives).
208
+ test/ Conformance test — replays the canonical fixtures + round-trips the vault.
209
+ \`\`\`
210
+
211
+ ## Use
212
+
213
+ \`\`\`bash
214
+ npm install
215
+ npm test # prove the toolchain + vault are conformant
216
+ npx ssss export vault --profile sale --out dist/bundle.ucw.json # a tradeable bundle
217
+ npx ssss inspect dist/bundle.ucw.json --files
218
+ npx ssss help portability # why a sale export is safe to share
219
+ \`\`\`
220
+
221
+ The \`sale\` profile ships every \`structural\` primitive and **drops** \`tenant_private\`
222
+ ones (the starter \`vault/tasks/\` file) — see \`ssss help portability\`.
223
+ ${withTR ? `
224
+ ## Total Recall (memory OS)
225
+
226
+ This project is also wired for [Total Recall](https://github.com/gregiteen/total-recall):
227
+
228
+ \`\`\`bash
229
+ npx -p total-recall-brain total-recall init # seed .agent/skills/total-recall
230
+ npx total-recall remember fact "..." # persist a fact
231
+ npx total-recall recall "..." # semantic recall
232
+ \`\`\`
233
+ ` : ''}`;
234
+ }
235
+
236
+ function claudeMd(name, withTR) {
237
+ return `# ${name} — agent guide
238
+
239
+ This repo uses **SSSS** (Structured Semantic Syntax System) as its state contract.
240
+
241
+ ## Source of truth
242
+ - \`vault/\` is the only source of truth — typed Markdown documents with YAML
243
+ frontmatter. Every file declares a \`type\` defined in the \`@ssss/cli\` core registry.
244
+ - Mutate the vault through the standard, not by hand-writing files in bulk:
245
+ \`npx ssss\` for bundle/tenant tooling, or the \`@ssss/cli/engine\` Operation Contract
246
+ in code.
247
+
248
+ ## Portability (§5.5) — the rule that matters
249
+ - \`structural\` documents (rules, workflows, pages, assistants) are the sellable model.
250
+ - \`tenant_private\` documents (tasks, customer data) are **never** shipped in a
251
+ \`template\`/\`sale\` export.
252
+ - Run \`npx ssss help portability\` before adding a new document type.
253
+
254
+ ## Verify before claiming done
255
+ - \`npm test\` replays the canonical Operation Contract fixtures through the engine
256
+ and round-trips \`vault/\` as a \`sale\` bundle. Keep it green.
257
+ ${withTR ? `
258
+ ## Memory (Total Recall)
259
+ - Persistent rules/decisions live in \`.agent/skills/total-recall\` via the
260
+ \`npx total-recall remember\` / \`recall\` CLI. Save corrections and decisions there.
261
+ ` : ''}`;
262
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * `ssss provision` — turn a `.ucw` bundle + parameters into a replayable plan of
3
+ * Operation Contract envelopes (spec §17). Pure: touches no filesystem. Resolves
4
+ * required parameters, checks link integrity under id-remap (§17.3), and assigns
5
+ * deterministic idempotency keys so a later `import` is a no-op on re-run.
6
+ */
7
+ import fs from 'node:fs';
8
+ import { provisionBundle } from '../src/bundle.mjs';
9
+ import { parseArgs, parseKeyValues, usage, die, wantsHelp } from './lib/cli.mjs';
10
+
11
+ const HELP = usage({
12
+ summary: 'Plan a bundle install: resolve params + link integrity → replayable envelopes (§17).',
13
+ usage: 'ssss provision <bundle.ucw.json> [options]',
14
+ options: [
15
+ ['--param <k=v>', 'Bind a bundle parameter (repeatable).'],
16
+ ['--workspace <id>', 'Target workspace id (default: ws-provision).'],
17
+ ['--prefix <path>', 'Place every file under this path prefix (id-remap).'],
18
+ ['--out <file>', 'Write the envelope plan here (default: stdout).'],
19
+ ['--dry-run', 'Mark every envelope dry_run (import will validate, not commit).'],
20
+ ],
21
+ examples: [
22
+ 'ssss provision festival.ucw.json --param business_name="Acme Fest" --param domain=acme.live',
23
+ 'ssss provision festival.ucw.json --workspace ws-42 --out plan.json',
24
+ ],
25
+ seeAlso: ['ssss import', 'ssss inspect', 'ssss help provisioning'],
26
+ });
27
+
28
+ export async function run(argv) {
29
+ if (wantsHelp(argv)) { console.log(HELP); return; }
30
+ const { positionals, flags } = parseArgs(argv, { booleans: ['dry-run'], multi: ['param'] });
31
+ const file = positionals[0];
32
+ if (!file) die('provision needs a bundle file.\n\n' + HELP);
33
+ if (!fs.existsSync(file)) die(`no such file: ${file}`);
34
+
35
+ let bundle;
36
+ try { bundle = JSON.parse(fs.readFileSync(file, 'utf8')); }
37
+ catch (err) { die(`not valid JSON: ${err.message}`); }
38
+
39
+ const result = provisionBundle(bundle, {
40
+ parameters: parseKeyValues(flags.param),
41
+ workspaceId: flags.workspace || 'ws-provision',
42
+ pathPrefix: flags.prefix || '',
43
+ dryRun: !!flags['dry-run'],
44
+ });
45
+
46
+ if (result.unresolved.length) {
47
+ console.error(`✗ unresolved required parameter(s): ${result.unresolved.join(', ')}`);
48
+ console.error(' supply them with --param key=value');
49
+ }
50
+ if (result.danglingLinks.length) {
51
+ console.error(`✗ ${result.danglingLinks.length} dangling internal link(s):`);
52
+ for (const d of result.danglingLinks) console.error(` - ${d.file} → [[${d.ref}]]`);
53
+ }
54
+ if (result.errors?.length) {
55
+ console.error(`✗ ${result.errors.length} bundle validation error(s):`);
56
+ for (const e of result.errors) console.error(` - ${e}`);
57
+ }
58
+
59
+ const plan = { workspace_id: flags.workspace || 'ws-provision', steps: result.steps, envelopes: result.plan };
60
+ const json = JSON.stringify(plan, null, 2);
61
+ if (flags.out) {
62
+ fs.writeFileSync(flags.out, json + '\n');
63
+ console.error(`Wrote ${result.plan.length} envelope(s) → ${flags.out}.`);
64
+ } else {
65
+ process.stdout.write(json + '\n');
66
+ }
67
+ if (!result.ok) process.exit(1);
68
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * `ssss validate` — check a `.ucw` bundle against the registry bundle schema
3
+ * (§16) and the §5.5 portability/profile conformance rules. Exits non-zero on
4
+ * any error so it can gate CI.
5
+ */
6
+ import fs from 'node:fs';
7
+ import { validateBundle } from '../src/bundle.mjs';
8
+ import { parseArgs, usage, die, wantsHelp } from './lib/cli.mjs';
9
+
10
+ const HELP = usage({
11
+ summary: 'Validate a .ucw bundle against the SSSS bundle schema (spec §16/§5.5).',
12
+ usage: 'ssss validate <bundle.ucw.json> [options]',
13
+ options: [
14
+ ['--registry <dir>', 'Registry directory (default: the package core registry).'],
15
+ ['--quiet', 'Print nothing on success; only exit code 0/1.'],
16
+ ],
17
+ examples: ['ssss validate festival.ucw.json'],
18
+ seeAlso: ['ssss export', 'ssss inspect', 'ssss help bundle'],
19
+ });
20
+
21
+ export async function run(argv) {
22
+ if (wantsHelp(argv)) { console.log(HELP); return; }
23
+ const { positionals, flags } = parseArgs(argv, { booleans: ['quiet'] });
24
+ const file = positionals[0];
25
+ if (!file) die('validate needs a bundle file.\n\n' + HELP);
26
+ if (!fs.existsSync(file)) die(`no such file: ${file}`);
27
+
28
+ let bundle;
29
+ try { bundle = JSON.parse(fs.readFileSync(file, 'utf8')); }
30
+ catch (err) { die(`not valid JSON: ${err.message}`); }
31
+
32
+ const { valid, errors } = validateBundle(bundle, { registryDir: flags.registry });
33
+ if (valid) {
34
+ if (!flags.quiet) {
35
+ const m = bundle.manifest || {};
36
+ console.log(`✓ valid — "${m.name}" · profile ${m.export_profile} · ${m.file_count} file(s) · core v${m.ssss_core_version}`);
37
+ }
38
+ return;
39
+ }
40
+ console.error(`✗ invalid — ${errors.length} error(s):`);
41
+ for (const e of errors) console.error(` - ${e}`);
42
+ process.exit(1);
43
+ }