simt-dev 0.6.0 → 0.6.3
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/CHANGELOG.md +71 -0
- package/README.md +44 -15
- package/bin/simt.mjs +19 -6
- package/docs/decisions/ADR-098-the-fourth-channel-is-one-file-and-it-needs-no-toolchain.md +1 -1
- package/docs/decisions/ADR-099-the-release-verifies-served-bytes-and-the-formula-mirrors-them.md +79 -0
- package/docs/decisions/ADR-100-a-staged-diff-is-the-last-gate-before-history-and-it-runs-inside-the-tool.md +94 -0
- package/docs/decisions/ADR-101-a-file-lease-belongs-to-one-session-and-the-pre-edit-hook-refuses-another.md +95 -0
- package/docs/decisions/ADR-102-the-two-queued-review-rows-are-closed-by-ruling.md +49 -0
- package/docs/decisions/ADR-103-co-projected-duplicates-merge-down.md +74 -0
- package/docs/decisions/ADR-104-the-pre-tool-use-policy-reads-the-command-the-way-a-shell-reads-it.md +76 -0
- package/docs/decisions/ADR-105-the-json-ceiling-shrinks-arrays-and-never-cuts-bytes.md +44 -0
- package/docs/decisions/README.md +44 -0
- package/package.json +1 -1
- package/rules/mdc/preview-postflight.mdc +5 -6
- package/rules/stacks/android.md +7 -8
- package/rules/stacks/cloudflare-workers.md +1 -1
- package/rules/stacks/container.md +2 -0
- package/rules/stacks/jvm.md +1 -1
- package/rules/stacks/react-native.md +3 -5
- package/rules/stacks/react-web.md +9 -9
- package/src/agenthooks/decide.mjs +101 -10
- package/src/agenthooks/policy.mjs +81 -5
- package/src/agenthooks/shell.mjs +523 -0
- package/src/argv.mjs +54 -9
- package/src/ci/workflow.mjs +7 -5
- package/src/commands/agenthook.mjs +25 -3
- package/src/commands/ci.mjs +28 -3
- package/src/commands/completions.mjs +27 -0
- package/src/commands/decide.mjs +14 -1
- package/src/commands/doctor.mjs +367 -0
- package/src/commands/explain.mjs +52 -0
- package/src/commands/help.mjs +66 -17
- package/src/commands/hooks.mjs +208 -60
- package/src/commands/ignore.mjs +152 -0
- package/src/commands/index.mjs +54 -100
- package/src/commands/init.mjs +82 -12
- package/src/commands/lease.mjs +136 -0
- package/src/commands/lessons.mjs +12 -0
- package/src/commands/mcp.mjs +2 -2
- package/src/commands/pack.mjs +9 -0
- package/src/commands/plan.mjs +13 -1
- package/src/commands/plugin.mjs +16 -1
- package/src/commands/prbody.mjs +45 -0
- package/src/commands/repoindex.mjs +3 -0
- package/src/commands/rules.mjs +145 -105
- package/src/commands/shared.mjs +36 -5
- package/src/commands/skills.mjs +293 -205
- package/src/commands/skillsinstall.mjs +100 -30
- package/src/commands/slice.mjs +353 -0
- package/src/commands/status.mjs +62 -0
- package/src/commands/sync.mjs +110 -15
- package/src/commands/undo.mjs +106 -0
- package/src/commands/upgrade.mjs +70 -8
- package/src/completions.mjs +191 -0
- package/src/diff.mjs +254 -0
- package/src/explain.mjs +258 -0
- package/src/federation/config.mjs +44 -0
- package/src/graph/load.mjs +14 -5
- package/src/hooks/dispatcher.mjs +110 -0
- package/src/hooks/gates.mjs +14 -3
- package/src/hooks/resolve.mjs +14 -0
- package/src/ignore.mjs +133 -0
- package/src/index/persist.mjs +66 -9
- package/src/journal.mjs +326 -0
- package/src/lease.mjs +113 -0
- package/src/mcp/server.mjs +15 -1
- package/src/mcp/tools.mjs +296 -256
- package/src/memory/decide.mjs +6 -1
- package/src/memory/lessons.mjs +12 -0
- package/src/output/index.mjs +82 -12
- package/src/package-version.mjs +49 -1
- package/src/packs/manage.mjs +13 -4
- package/src/plan/index.mjs +6 -0
- package/src/plugins.mjs +27 -6
- package/src/prbody.mjs +251 -0
- package/src/projection/manifest.mjs +13 -2
- package/src/projection/shared.mjs +4 -1
- package/src/secrets.mjs +231 -0
- package/src/status.mjs +152 -0
package/src/argv.mjs
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// Anything unrecognised is an error, not a silent ignore: a typo'd gate name
|
|
11
11
|
// that is ignored is worse than a typo'd gate name that fails.
|
|
12
12
|
|
|
13
|
-
import { EXIT
|
|
13
|
+
import { EXIT } from './output/index.mjs';
|
|
14
14
|
|
|
15
15
|
// `--quiet`, `--semantic` and `--mermaid` were here and were read by nothing (review
|
|
16
16
|
// 0001 filed it; it survived every change to this file since). Each was a plan's
|
|
@@ -93,6 +93,18 @@ export const BOOL_FLAGS = new Set([
|
|
|
93
93
|
// the deterministic outline. Off by default: no model in the default path (P1),
|
|
94
94
|
// and a model that is not asked for should not be opened against (ADR-090).
|
|
95
95
|
'local',
|
|
96
|
+
// doctor: apply the remedies flagged safe (local, idempotent, journaled) and
|
|
97
|
+
// list the rest. Off by default because a repair, however safe, is still a
|
|
98
|
+
// write, and a command whose name is a diagnosis should not write by default.
|
|
99
|
+
'fix',
|
|
100
|
+
// status: print the one-line gate summary from the last check. Off by
|
|
101
|
+
// default because the only mode this command has is the line; bare `status`
|
|
102
|
+
// is a usage error naming it, the way `plan` without a subcommand is.
|
|
103
|
+
'line',
|
|
104
|
+
// every writer: print the plan as unified diffs. Off by default because a
|
|
105
|
+
// plan report is the compact answer and the diff is the verbose one; --diff
|
|
106
|
+
// never changes what the run writes or its exit code, only what it shows.
|
|
107
|
+
'diff',
|
|
96
108
|
]);
|
|
97
109
|
|
|
98
110
|
export const VALUE_FLAGS = new Set([
|
|
@@ -138,6 +150,15 @@ export const VALUE_FLAGS = new Set([
|
|
|
138
150
|
// plan done: the test command that proves the slice. Plan 0003 R31.
|
|
139
151
|
'test',
|
|
140
152
|
'state',
|
|
153
|
+
// slice finish: the Conventional Commits type for the commit it makes. A value,
|
|
154
|
+
// not a boolean, because a row title cannot say whether the slice was a feat, a
|
|
155
|
+
// fix or a chore, and deriving it from the words would be a guess no reader of
|
|
156
|
+
// the message could argue with (plan 0003 R53).
|
|
157
|
+
'type',
|
|
158
|
+
// lease: how long the claim lasts, in seconds. A value, because "a lease is short"
|
|
159
|
+
// is not a number any reader can act on, and an expiry nobody chose is an expiry
|
|
160
|
+
// nobody can plan around (plan 0003 R54, ADR-101).
|
|
161
|
+
'ttl',
|
|
141
162
|
// map: outline (paths), names (exported symbols) or signatures (the line).
|
|
142
163
|
// A boolean would not say which of the three the caller wanted.
|
|
143
164
|
'level',
|
|
@@ -151,6 +172,10 @@ export const VALUE_FLAGS = new Set([
|
|
|
151
172
|
// "generate a package" with no destination would have to guess, and a guess about where
|
|
152
173
|
// a directory tree goes is the one failure that leaves files behind.
|
|
153
174
|
'out',
|
|
175
|
+
// pr-body: the ref the body diffs against. A value, because the fork point is a
|
|
176
|
+
// name only the author's branch knows, and the default (origin/main, then main)
|
|
177
|
+
// is a guess the report states out loud rather than a question it asks.
|
|
178
|
+
'base',
|
|
154
179
|
]);
|
|
155
180
|
|
|
156
181
|
/**
|
|
@@ -168,9 +193,29 @@ export const VALUE_FLAGS = new Set([
|
|
|
168
193
|
*/
|
|
169
194
|
export const REPEATABLE_FLAGS = new Set(['skill', 'arm', 'compare', 'supersedes']);
|
|
170
195
|
|
|
196
|
+
/**
|
|
197
|
+
* Whether a raw argv asks for JSON, read before parsing succeeds.
|
|
198
|
+
*
|
|
199
|
+
* A parse failure is still a result the caller may need as JSON (Q11), but the
|
|
200
|
+
* parsed flags do not exist yet — so the dispatcher reads the spelling straight
|
|
201
|
+
* off argv. Only the exact `--json` spelling counts, and nothing past `--` does:
|
|
202
|
+
* a positional that happens to read `--json` is a path, not a flag.
|
|
203
|
+
*/
|
|
204
|
+
export function rawJsonFlag(argv) {
|
|
205
|
+
for (const arg of argv) {
|
|
206
|
+
if (arg === '--') break;
|
|
207
|
+
if (arg === '--json') return true;
|
|
208
|
+
if (arg === '--no-json') return false;
|
|
209
|
+
}
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
|
|
171
213
|
/**
|
|
172
214
|
* @param {string[]} argv raw arguments, excluding node and the script path
|
|
173
|
-
* @returns {{ok: true, value: object} | {ok: false, code: number}}
|
|
215
|
+
* @returns {{ok: true, value: object} | {ok: false, error: {message: string, code: number}}}
|
|
216
|
+
*
|
|
217
|
+
* A failure is data, never output: reporting it is the caller's job, because only
|
|
218
|
+
* the caller knows whether `--json` was asked for (Q11). Nothing here prints.
|
|
174
219
|
*/
|
|
175
220
|
export function parse(argv) {
|
|
176
221
|
const flags = {};
|
|
@@ -196,14 +241,14 @@ export function parse(argv) {
|
|
|
196
241
|
if (raw.startsWith('no-')) {
|
|
197
242
|
const positive = raw.slice(3);
|
|
198
243
|
if (!BOOL_FLAGS.has(positive)) {
|
|
199
|
-
return { ok: false,
|
|
244
|
+
return { ok: false, error: usage(`unknown flag --${raw}`) };
|
|
200
245
|
}
|
|
201
246
|
flags[camel(positive)] = false;
|
|
202
247
|
continue;
|
|
203
248
|
}
|
|
204
249
|
if (BOOL_FLAGS.has(raw)) {
|
|
205
250
|
if (inlineValue !== undefined) {
|
|
206
|
-
return { ok: false,
|
|
251
|
+
return { ok: false, error: usage(`--${raw} takes no value`) };
|
|
207
252
|
}
|
|
208
253
|
flags[camel(raw)] = true;
|
|
209
254
|
continue;
|
|
@@ -211,19 +256,19 @@ export function parse(argv) {
|
|
|
211
256
|
if (VALUE_FLAGS.has(raw)) {
|
|
212
257
|
const value = inlineValue ?? argv[i + 1];
|
|
213
258
|
if (value === undefined || (inlineValue === undefined && value.startsWith('--'))) {
|
|
214
|
-
return { ok: false,
|
|
259
|
+
return { ok: false, error: usage(`--${raw} requires a value`) };
|
|
215
260
|
}
|
|
216
261
|
if (inlineValue === undefined) i += 1;
|
|
217
262
|
if (raw === 'budget' || raw === 'max-bytes') {
|
|
218
263
|
const n = Number(value);
|
|
219
264
|
if (!Number.isInteger(n) || n < 0) {
|
|
220
|
-
return { ok: false,
|
|
265
|
+
return { ok: false, error: usage(`--${raw} must be a non-negative integer, received ${value}`) };
|
|
221
266
|
}
|
|
222
267
|
flags[camel(raw)] = n;
|
|
223
268
|
} else if (raw === 'reps') {
|
|
224
269
|
const n = Number(value);
|
|
225
270
|
if (!Number.isInteger(n) || n < 1) {
|
|
226
|
-
return { ok: false,
|
|
271
|
+
return { ok: false, error: usage(`--reps must be a positive integer, received ${value}`) };
|
|
227
272
|
}
|
|
228
273
|
flags.reps = n;
|
|
229
274
|
} else {
|
|
@@ -234,7 +279,7 @@ export function parse(argv) {
|
|
|
234
279
|
}
|
|
235
280
|
continue;
|
|
236
281
|
}
|
|
237
|
-
return { ok: false,
|
|
282
|
+
return { ok: false, error: usage(`unknown flag --${raw}`) };
|
|
238
283
|
}
|
|
239
284
|
|
|
240
285
|
return { ok: true, value: { flags, positional } };
|
|
@@ -253,5 +298,5 @@ function camel(name) {
|
|
|
253
298
|
}
|
|
254
299
|
|
|
255
300
|
function usage(message) {
|
|
256
|
-
return
|
|
301
|
+
return { message, code: EXIT.USAGE };
|
|
257
302
|
}
|
package/src/ci/workflow.mjs
CHANGED
|
@@ -128,13 +128,15 @@ function header(own) {
|
|
|
128
128
|
* is the release a reader recognises, and Dependabot's `github-actions` ecosystem
|
|
129
129
|
* rewrites the pair together - which is why the pin needs no maintenance here.
|
|
130
130
|
*
|
|
131
|
-
* The major version
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
131
|
+
* The major version moves by review, not by updater. `checkout` stayed on v4 while v7
|
|
132
|
+
* carried breaking changes a gate should not absorb by re-running Dependabot; it moved
|
|
133
|
+
* to v7.0.1 when the v7 line had run green in this repository's own workflows (the
|
|
134
|
+
* checkout steps of the v7 Dependabot run all succeeded — only the template-drift
|
|
135
|
+
* tests failed, which is the pin doing its job). `setup-node` stays on v4: no run has
|
|
136
|
+
* yet proven a newer major here, and moving it is the same one reviewed line.
|
|
135
137
|
*/
|
|
136
138
|
export const ACTIONS = {
|
|
137
|
-
checkout: { repo: 'actions/checkout', commit: '
|
|
139
|
+
checkout: { repo: 'actions/checkout', commit: '3d3c42e5aac5ba805825da76410c181273ba90b1', release: 'v7.0.1' },
|
|
138
140
|
setupNode: { repo: 'actions/setup-node', commit: '49933ea5288caeca8642d1e84afbd3f7d6820020', release: 'v4.4.0' },
|
|
139
141
|
};
|
|
140
142
|
|
|
@@ -22,8 +22,10 @@
|
|
|
22
22
|
// tool call. Failures of this command exit 1, which a harness reports as a failed hook,
|
|
23
23
|
// and that is the right blast radius for a hook that is broken rather than refusing.
|
|
24
24
|
|
|
25
|
-
import { mkdir, readFile
|
|
25
|
+
import { mkdir, readFile } from 'node:fs/promises';
|
|
26
26
|
import { dirname, join } from 'node:path';
|
|
27
|
+
import { commitJournal, journaledWriteFile } from '../journal.mjs';
|
|
28
|
+
import { diffField, emitDiffs, readBefore } from '../diff.mjs';
|
|
27
29
|
|
|
28
30
|
import { EXIT, emit, emitJson, fail, isJson, note, writeMode } from '../output/index.mjs';
|
|
29
31
|
import { CLI } from '../name.mjs';
|
|
@@ -185,6 +187,17 @@ async function installHooks(root, flags) {
|
|
|
185
187
|
|
|
186
188
|
const refused = plan.filter((p) => p.error);
|
|
187
189
|
|
|
190
|
+
// The `--diff` pairs, read before anything is written so the before side is
|
|
191
|
+
// true in preview and in apply alike (R50).
|
|
192
|
+
const diffs =
|
|
193
|
+
flags.diff === true
|
|
194
|
+
? await Promise.all(
|
|
195
|
+
plan
|
|
196
|
+
.filter((p) => p.action !== 'unchanged')
|
|
197
|
+
.map(async (p) => ({ path: p.path, before: await readBefore(join(root, p.path)), after: p.text })),
|
|
198
|
+
)
|
|
199
|
+
: [];
|
|
200
|
+
|
|
188
201
|
if (!mode.apply) {
|
|
189
202
|
if (isJson(flags)) {
|
|
190
203
|
emitJson({
|
|
@@ -195,6 +208,7 @@ async function installHooks(root, flags) {
|
|
|
195
208
|
plan: plan.map(({ text, ...rest }) => rest),
|
|
196
209
|
notWritten: others,
|
|
197
210
|
filesWritten: false,
|
|
211
|
+
...diffField(diffs, flags),
|
|
198
212
|
});
|
|
199
213
|
return refused.length > 0 ? EXIT.FAIL : EXIT.OK;
|
|
200
214
|
}
|
|
@@ -207,10 +221,12 @@ async function installHooks(root, flags) {
|
|
|
207
221
|
'',
|
|
208
222
|
refused.length > 0
|
|
209
223
|
? `REFUSED ${refused.length} config file(s) could not be read; nothing written`
|
|
210
|
-
:
|
|
224
|
+
: `PLAN nothing written; apply with: ${CLI} agent-hooks install --yes`,
|
|
211
225
|
].join('\n'),
|
|
212
226
|
{ maxBytes: flags.maxBytes ?? 0 },
|
|
213
227
|
);
|
|
228
|
+
// After the report: the report says what, the diff shows it (R50).
|
|
229
|
+
emitDiffs(diffs, flags);
|
|
214
230
|
return refused.length > 0 ? EXIT.FAIL : EXIT.OK;
|
|
215
231
|
}
|
|
216
232
|
|
|
@@ -221,12 +237,15 @@ async function installHooks(root, flags) {
|
|
|
221
237
|
return fail(`refused to write ${refused.map((p) => `${p.path} (${p.error})`).join('; ')}`, EXIT.FAIL);
|
|
222
238
|
}
|
|
223
239
|
|
|
240
|
+
// Journaled so `undo` restores this run (R49).
|
|
241
|
+
const journal = [];
|
|
224
242
|
const written = [];
|
|
225
243
|
for (const p of plan.filter((x) => x.action !== 'unchanged')) {
|
|
226
244
|
await mkdir(join(root, dirname(p.path)), { recursive: true });
|
|
227
|
-
await
|
|
245
|
+
await journaledWriteFile(root, journal, join(root, p.path), p.text);
|
|
228
246
|
written.push(p);
|
|
229
247
|
}
|
|
248
|
+
await commitJournal(root, 'agent-hooks install', journal);
|
|
230
249
|
|
|
231
250
|
const notes = [];
|
|
232
251
|
for (const p of written) {
|
|
@@ -245,6 +264,7 @@ async function installHooks(root, flags) {
|
|
|
245
264
|
written: written.map((p) => p.path),
|
|
246
265
|
filesWritten: true,
|
|
247
266
|
notes,
|
|
267
|
+
...diffField(diffs, flags),
|
|
248
268
|
});
|
|
249
269
|
return EXIT.OK;
|
|
250
270
|
}
|
|
@@ -259,6 +279,8 @@ async function installHooks(root, flags) {
|
|
|
259
279
|
].join('\n'),
|
|
260
280
|
{ maxBytes: flags.maxBytes ?? 0 },
|
|
261
281
|
);
|
|
282
|
+
// After the report: the report says what, the diff shows it (R50).
|
|
283
|
+
emitDiffs(diffs, flags);
|
|
262
284
|
for (const o of others) note(`note ${o.id}: not written - ${o.note}`);
|
|
263
285
|
for (const n of notes) note(`note ${n}`);
|
|
264
286
|
note(`note never committed or pushed (ADR-001); review, then commit yourself`);
|
package/src/commands/ci.mjs
CHANGED
|
@@ -18,8 +18,10 @@
|
|
|
18
18
|
// commands owns which file would be learning about ownership twice to learn nothing
|
|
19
19
|
// new, and the next .github file would arrive with a third command to remember.
|
|
20
20
|
|
|
21
|
-
import { mkdir, readFile
|
|
21
|
+
import { mkdir, readFile } from 'node:fs/promises';
|
|
22
22
|
import { dirname } from 'node:path';
|
|
23
|
+
import { commitJournal, journaledWriteFile } from '../journal.mjs';
|
|
24
|
+
import { diffField, emitDiffs, readBefore } from '../diff.mjs';
|
|
23
25
|
|
|
24
26
|
import { EXIT, emit, emitJson, isJson, note, writeMode } from '../output/index.mjs';
|
|
25
27
|
import { planWorkflow, renderWorkflow, WORKFLOW_FILE } from '../ci/workflow.mjs';
|
|
@@ -121,18 +123,37 @@ export async function ciCommand(positional, flags) {
|
|
|
121
123
|
return EXIT.FAIL;
|
|
122
124
|
}
|
|
123
125
|
|
|
126
|
+
// The `--diff` pairs, read before anything is written so the before side is
|
|
127
|
+
// true in preview and in apply alike (R50).
|
|
128
|
+
const renders = new Map(targets().map(({ key, render }) => [key, render]));
|
|
129
|
+
const diffs =
|
|
130
|
+
flags.diff === true
|
|
131
|
+
? await Promise.all(
|
|
132
|
+
plans
|
|
133
|
+
.filter((p) => p.action !== 'unchanged')
|
|
134
|
+
.map(async (p) => ({ path: p.file, before: await readBefore(p.path), after: renders.get(p.key)(p) })),
|
|
135
|
+
)
|
|
136
|
+
: [];
|
|
137
|
+
|
|
124
138
|
if (isJson(flags)) {
|
|
125
|
-
emitJson(
|
|
139
|
+
emitJson(
|
|
140
|
+
{ ok: true, command: 'ci', root, files: todo.map(publicPlan), written: false, dryRun: !mode.apply, ...diffField(diffs, flags) },
|
|
141
|
+
{},
|
|
142
|
+
);
|
|
126
143
|
} else {
|
|
127
144
|
emit(lines.join('\n'));
|
|
128
145
|
}
|
|
129
146
|
|
|
130
147
|
if (!mode.apply) {
|
|
148
|
+
// After the report: the report says what, the diff shows it (R50).
|
|
149
|
+
emitDiffs(diffs, flags);
|
|
131
150
|
note('');
|
|
132
151
|
note(mode.dryRun ? 'dry-run nothing written' : `dry-run nothing written; apply with: ${CLI} ci --yes`);
|
|
133
152
|
return EXIT.OK;
|
|
134
153
|
}
|
|
135
154
|
|
|
155
|
+
// Journaled so `undo` restores this run (R49).
|
|
156
|
+
const journal = [];
|
|
136
157
|
for (const { key, plan, render } of targets()) {
|
|
137
158
|
const p = plans.find((x) => x.key === key);
|
|
138
159
|
if (p.action === 'unchanged') continue;
|
|
@@ -140,9 +161,10 @@ export async function ciCommand(positional, flags) {
|
|
|
140
161
|
// The renderer takes the plan because detection-driven content is a function of
|
|
141
162
|
// this repo's facts: a dependabot config rendered without them would watch
|
|
142
163
|
// nothing, which is the same silent-nothing the detection ruling exists to stop.
|
|
143
|
-
await
|
|
164
|
+
await journaledWriteFile(root, journal, p.path, render(p));
|
|
144
165
|
note(`wrote ${p.file} (${p.action}; the template and this copy now agree)`);
|
|
145
166
|
}
|
|
167
|
+
await commitJournal(root, 'ci', journal);
|
|
146
168
|
if (isJson(flags)) {
|
|
147
169
|
emitJson(
|
|
148
170
|
{
|
|
@@ -151,10 +173,13 @@ export async function ciCommand(positional, flags) {
|
|
|
151
173
|
root,
|
|
152
174
|
files: plans.map((p) => ({ file: p.file, action: p.action, bytes: p.bytes })),
|
|
153
175
|
written: true,
|
|
176
|
+
...diffField(diffs, flags),
|
|
154
177
|
},
|
|
155
178
|
{},
|
|
156
179
|
);
|
|
157
180
|
}
|
|
181
|
+
// After the report: the report says what, the diff shows it (R50).
|
|
182
|
+
emitDiffs(diffs, flags);
|
|
158
183
|
return EXIT.OK;
|
|
159
184
|
}
|
|
160
185
|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// `simt completions <shell>` — print a completion script (plan 0003 R51).
|
|
2
|
+
//
|
|
3
|
+
// The script goes to stdout and nothing else does, because the install is a
|
|
4
|
+
// redirect: `source <(simt completions bash)`, or the zsh file dropped onto
|
|
5
|
+
// `$fpath`. A report line above it would be a report line in the user's shell
|
|
6
|
+
// config.
|
|
7
|
+
|
|
8
|
+
import { EXIT, emit, emitJson, fail, isJson } from '../output/index.mjs';
|
|
9
|
+
import { SHELLS, renderCompletions } from '../completions.mjs';
|
|
10
|
+
|
|
11
|
+
export async function completionsCommand(args, flags, commands) {
|
|
12
|
+
const shell = args[0];
|
|
13
|
+
if (shell === undefined) {
|
|
14
|
+
return fail('completions needs a shell', EXIT.USAGE, `one of: ${SHELLS.join(', ')}`);
|
|
15
|
+
}
|
|
16
|
+
if (!SHELLS.includes(shell)) {
|
|
17
|
+
return fail(`unknown shell \`${shell}\``, EXIT.USAGE, `one of: ${SHELLS.join(', ')}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const script = renderCompletions(shell, commands);
|
|
21
|
+
if (isJson(flags)) {
|
|
22
|
+
emitJson({ ok: true, command: 'completions', shell, script });
|
|
23
|
+
return EXIT.OK;
|
|
24
|
+
}
|
|
25
|
+
emit(script, { maxBytes: flags.maxBytes ?? 0 });
|
|
26
|
+
return EXIT.OK;
|
|
27
|
+
}
|
package/src/commands/decide.mjs
CHANGED
|
@@ -7,6 +7,8 @@ import { EXIT, emit, emitJson, fail, isJson, note, writeMode } from '../output/i
|
|
|
7
7
|
import { CLI } from '../name.mjs';
|
|
8
8
|
import { DECISIONS_DIR } from '../decisions/index.mjs';
|
|
9
9
|
import { lintDecisions, scaffoldDecision } from '../memory/decide.mjs';
|
|
10
|
+
import { commitJournal } from '../journal.mjs';
|
|
11
|
+
import { diffField, emitDiffs } from '../diff.mjs';
|
|
10
12
|
import { parsePathList } from '../argv.mjs';
|
|
11
13
|
|
|
12
14
|
export async function decideCommand(args, flags) {
|
|
@@ -49,12 +51,17 @@ export async function decideCommand(args, flags) {
|
|
|
49
51
|
const parsedPaths = parsePathList(flags.paths);
|
|
50
52
|
const explicitPaths = flags.paths !== undefined ? parsedPaths : undefined;
|
|
51
53
|
|
|
54
|
+
// Journaled so `undo` restores the scaffold (R49); committed only when the
|
|
55
|
+
// run actually wrote, which commitJournal already guarantees.
|
|
56
|
+
const journal = [];
|
|
52
57
|
const res = await scaffoldDecision(root, {
|
|
53
58
|
title,
|
|
54
59
|
affects: explicitPaths,
|
|
55
60
|
supersedes: flags.supersedes,
|
|
56
61
|
write: mode.apply,
|
|
62
|
+
journal: mode.apply ? journal : undefined,
|
|
57
63
|
});
|
|
64
|
+
if (mode.apply && res.ok) await commitJournal(root, 'decide', journal);
|
|
58
65
|
|
|
59
66
|
if (!res.ok) {
|
|
60
67
|
return fail(
|
|
@@ -70,6 +77,10 @@ export async function decideCommand(args, flags) {
|
|
|
70
77
|
note('dry-run no files written');
|
|
71
78
|
}
|
|
72
79
|
|
|
80
|
+
// A scaffold always creates: the number is free by construction, so the
|
|
81
|
+
// before side is empty and the pair is the whole file (R50).
|
|
82
|
+
const diffs = flags.diff === true ? [{ path: res.relPath, before: null, after: res.text }] : [];
|
|
83
|
+
|
|
73
84
|
if (isJson(flags)) {
|
|
74
85
|
emitJson({
|
|
75
86
|
ok: true,
|
|
@@ -84,10 +95,10 @@ export async function decideCommand(args, flags) {
|
|
|
84
95
|
supersedes: res.supersedes,
|
|
85
96
|
written: res.written,
|
|
86
97
|
dryRun: !mode.apply,
|
|
98
|
+
...diffField(diffs, flags),
|
|
87
99
|
});
|
|
88
100
|
return EXIT.OK;
|
|
89
101
|
}
|
|
90
|
-
|
|
91
102
|
const supersedesLabel =
|
|
92
103
|
res.supersedes.length > 0
|
|
93
104
|
? res.supersedes
|
|
@@ -112,5 +123,7 @@ export async function decideCommand(args, flags) {
|
|
|
112
123
|
];
|
|
113
124
|
|
|
114
125
|
emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
|
|
126
|
+
// After the report: the report says what, the diff shows it (R50).
|
|
127
|
+
emitDiffs(diffs, flags);
|
|
115
128
|
return EXIT.OK;
|
|
116
129
|
}
|