@rsaglobaltech/specgate 0.27.9 → 1.0.0-rc.2
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 +57 -0
- package/dist/packages/core/src/application/ValidateChangeUseCase.js +19 -3
- package/dist/packages/core/src/application/ValidateChangeUseCase.js.map +1 -1
- package/dist/packages/core/src/domain/ApiContract.js +157 -0
- package/dist/packages/core/src/domain/ApiContract.js.map +1 -0
- package/dist/packages/core/src/domain/Approvals.js +127 -0
- package/dist/packages/core/src/domain/Approvals.js.map +1 -0
- package/dist/packages/core/src/domain/ChangeLayers.js +285 -0
- package/dist/packages/core/src/domain/ChangeLayers.js.map +1 -0
- package/dist/packages/core/src/domain/HarnessConfig.js.map +1 -1
- package/dist/packages/core/src/domain/HarnessGaps.js +182 -0
- package/dist/packages/core/src/domain/HarnessGaps.js.map +1 -0
- package/dist/packages/core/src/domain/HarnessReport.js +16 -1
- package/dist/packages/core/src/domain/HarnessReport.js.map +1 -1
- package/dist/packages/core/src/domain/HarnessRun.js.map +1 -1
- package/dist/packages/core/src/domain/Principles.js +119 -0
- package/dist/packages/core/src/domain/Principles.js.map +1 -0
- package/dist/packages/core/src/domain/RequirementGraph.js +12 -1
- package/dist/packages/core/src/domain/RequirementGraph.js.map +1 -1
- package/dist/packages/core/src/domain/Workspace.js +150 -0
- package/dist/packages/core/src/domain/Workspace.js.map +1 -0
- package/dist/packages/core/src/domain/WorkspaceStory.js +176 -0
- package/dist/packages/core/src/domain/WorkspaceStory.js.map +1 -0
- package/dist/packages/core/src/index.js +1 -0
- package/dist/packages/core/src/index.js.map +1 -1
- package/dist/packages/core/src/infrastructure/HarnessConfigFile.js +12 -0
- package/dist/packages/core/src/infrastructure/HarnessConfigFile.js.map +1 -1
- package/dist/packages/mcp-spec-driven/src/tools.js +18 -0
- package/dist/packages/mcp-spec-driven/src/tools.js.map +1 -1
- package/dist/scripts/alm/cli.js +45 -4
- package/dist/scripts/alm/cli.js.map +1 -1
- package/dist/scripts/alm/clients.js +3 -1
- package/dist/scripts/alm/clients.js.map +1 -1
- package/dist/scripts/alm/phases.js +279 -0
- package/dist/scripts/alm/phases.js.map +1 -0
- package/dist/scripts/alm/port.js +33 -5
- package/dist/scripts/alm/port.js.map +1 -1
- package/dist/scripts/alm/providers/azure.js +72 -11
- package/dist/scripts/alm/providers/azure.js.map +1 -1
- package/dist/scripts/alm/providers/github.js +67 -7
- package/dist/scripts/alm/providers/github.js.map +1 -1
- package/dist/scripts/alm/providers/index.js +8 -2
- package/dist/scripts/alm/providers/index.js.map +1 -1
- package/dist/scripts/alm/providers/jira.js +86 -8
- package/dist/scripts/alm/providers/jira.js.map +1 -1
- package/dist/scripts/alm/providers/linear.js +167 -0
- package/dist/scripts/alm/providers/linear.js.map +1 -0
- package/dist/scripts/change/artifacts.js +15 -1
- package/dist/scripts/change/artifacts.js.map +1 -1
- package/dist/scripts/change/instructions.js +47 -5
- package/dist/scripts/change/instructions.js.map +1 -1
- package/dist/scripts/cli/commands/alm/PhaseCommand.js +153 -0
- package/dist/scripts/cli/commands/alm/PhaseCommand.js.map +1 -0
- package/dist/scripts/cli/commands/alm/PullCommand.js +33 -2
- package/dist/scripts/cli/commands/alm/PullCommand.js.map +1 -1
- package/dist/scripts/cli/commands/alm/pull-back.js +104 -0
- package/dist/scripts/cli/commands/alm/pull-back.js.map +1 -0
- package/dist/scripts/cli/commands/change/ChangeCommand.js +149 -15
- package/dist/scripts/cli/commands/change/ChangeCommand.js.map +1 -1
- package/dist/scripts/cli/commands/change/TicketCommand.js +20 -7
- package/dist/scripts/cli/commands/change/TicketCommand.js.map +1 -1
- package/dist/scripts/cli/commands/harness/RunCommand.js +64 -20
- package/dist/scripts/cli/commands/harness/RunCommand.js.map +1 -1
- package/dist/scripts/cli/commands/project/AdoptCommand.js +44 -4
- package/dist/scripts/cli/commands/project/AdoptCommand.js.map +1 -1
- package/dist/scripts/cli/commands/project/InitProjectCommand.js +9 -14
- package/dist/scripts/cli/commands/project/InitProjectCommand.js.map +1 -1
- package/dist/scripts/cli/commands/project/MigrateCommand.js +166 -0
- package/dist/scripts/cli/commands/project/MigrateCommand.js.map +1 -0
- package/dist/scripts/cli/commands/project/OnboardCommand.js +23 -5
- package/dist/scripts/cli/commands/project/OnboardCommand.js.map +1 -1
- package/dist/scripts/cli/commands/quality/CheckCommand.js +25 -0
- package/dist/scripts/cli/commands/quality/CheckCommand.js.map +1 -1
- package/dist/scripts/cli/commands/quality/ValidateSpecsCommand.js +15 -0
- package/dist/scripts/cli/commands/quality/ValidateSpecsCommand.js.map +1 -1
- package/dist/scripts/cli/commands/spec/ContractCommand.js +117 -0
- package/dist/scripts/cli/commands/spec/ContractCommand.js.map +1 -0
- package/dist/scripts/cli/commands/spec/DoneCommand.js +22 -0
- package/dist/scripts/cli/commands/spec/DoneCommand.js.map +1 -1
- package/dist/scripts/cli/commands/spec/DraftCommand.js +22 -8
- package/dist/scripts/cli/commands/spec/DraftCommand.js.map +1 -1
- package/dist/scripts/cli/commands/spec/DriftCommand.js +128 -0
- package/dist/scripts/cli/commands/spec/DriftCommand.js.map +1 -0
- package/dist/scripts/cli/commands/spec/StatusCommand.js +85 -0
- package/dist/scripts/cli/commands/spec/StatusCommand.js.map +1 -1
- package/dist/scripts/cli/commands/spec/StudioCommand.js +48 -6
- package/dist/scripts/cli/commands/spec/StudioCommand.js.map +1 -1
- package/dist/scripts/cli/commands/workspace/WorkspaceCommand.js +612 -0
- package/dist/scripts/cli/commands/workspace/WorkspaceCommand.js.map +1 -0
- package/dist/scripts/contract.js +10 -0
- package/dist/scripts/contract.js.map +1 -0
- package/dist/scripts/docs/nav.js +16 -1
- package/dist/scripts/docs/nav.js.map +1 -1
- package/dist/scripts/draft/drafting-guide.js +6 -0
- package/dist/scripts/draft/drafting-guide.js.map +1 -1
- package/dist/scripts/draft/ticket-guide.js +5 -0
- package/dist/scripts/draft/ticket-guide.js.map +1 -1
- package/dist/scripts/drift.js +10 -0
- package/dist/scripts/drift.js.map +1 -0
- package/dist/scripts/harness/gap-evidence.js +149 -0
- package/dist/scripts/harness/gap-evidence.js.map +1 -0
- package/dist/scripts/harness/prompt.js +54 -1
- package/dist/scripts/harness/prompt.js.map +1 -1
- package/dist/scripts/harness/report.js +104 -5
- package/dist/scripts/harness/report.js.map +1 -1
- package/dist/scripts/lib/approvals.js +243 -0
- package/dist/scripts/lib/approvals.js.map +1 -0
- package/dist/scripts/lib/change-review.js +19 -11
- package/dist/scripts/lib/change-review.js.map +1 -1
- package/dist/scripts/lib/contracts.js +211 -0
- package/dist/scripts/lib/contracts.js.map +1 -0
- package/dist/scripts/lib/drift.js +80 -0
- package/dist/scripts/lib/drift.js.map +1 -0
- package/dist/scripts/lib/live-model.js +1 -0
- package/dist/scripts/lib/live-model.js.map +1 -1
- package/dist/scripts/lib/migrate.js +264 -0
- package/dist/scripts/lib/migrate.js.map +1 -0
- package/dist/scripts/lib/role-packages.js +230 -0
- package/dist/scripts/lib/role-packages.js.map +1 -0
- package/dist/scripts/lib/steering.js +180 -0
- package/dist/scripts/lib/steering.js.map +1 -0
- package/dist/scripts/lib/studio-app-page.js +28 -5
- package/dist/scripts/lib/studio-app-page.js.map +1 -1
- package/dist/scripts/lib/studio-app.js +29 -0
- package/dist/scripts/lib/studio-app.js.map +1 -1
- package/dist/scripts/lib/studio-route.js +20 -0
- package/dist/scripts/lib/studio-route.js.map +1 -1
- package/dist/scripts/lib/surface.js +55 -0
- package/dist/scripts/lib/surface.js.map +1 -1
- package/dist/scripts/lib/workspace-draft.js +146 -0
- package/dist/scripts/lib/workspace-draft.js.map +1 -0
- package/dist/scripts/lib/workspace-live-page.js +90 -0
- package/dist/scripts/lib/workspace-live-page.js.map +1 -0
- package/dist/scripts/lib/workspace-live.js +199 -0
- package/dist/scripts/lib/workspace-live.js.map +1 -0
- package/dist/scripts/lib/workspace.js +619 -0
- package/dist/scripts/lib/workspace.js.map +1 -0
- package/dist/scripts/migrate.js +10 -0
- package/dist/scripts/migrate.js.map +1 -0
- package/dist/scripts/schema/registry.js +12 -0
- package/dist/scripts/schema/registry.js.map +1 -1
- package/dist/scripts/workspace.js +10 -0
- package/dist/scripts/workspace.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,612 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.WorkspaceCommand = void 0;
|
|
37
|
+
const fs = __importStar(require("node:fs"));
|
|
38
|
+
const path = __importStar(require("node:path"));
|
|
39
|
+
const command_1 = require("../../../lib/command");
|
|
40
|
+
const agent_1 = require("../../../lib/agent");
|
|
41
|
+
const diagnostics_1 = require("../../../lib/diagnostics");
|
|
42
|
+
const sonar_local_1 = require("../../../lib/sonar-local");
|
|
43
|
+
const ui = __importStar(require("../../../lib/ui"));
|
|
44
|
+
const ui_1 = require("../../../lib/ui");
|
|
45
|
+
const Workspace_1 = require("../../../../packages/core/src/domain/Workspace");
|
|
46
|
+
const WorkspaceStory_1 = require("../../../../packages/core/src/domain/WorkspaceStory");
|
|
47
|
+
const workspace_draft_1 = require("../../../lib/workspace-draft");
|
|
48
|
+
const workspace_1 = require("../../../lib/workspace");
|
|
49
|
+
const workspace_live_1 = require("../../../lib/workspace-live");
|
|
50
|
+
const RunCommand_1 = require("../harness/RunCommand");
|
|
51
|
+
const node_child_process_1 = require("node:child_process");
|
|
52
|
+
const project_root_1 = require("../../../lib/project-root");
|
|
53
|
+
const NULL_SHAPE = { workspace: null };
|
|
54
|
+
function usage() {
|
|
55
|
+
process.stdout.write(`
|
|
56
|
+
🧩 ${ui_1.c.bold}specgate workspace${ui_1.c.reset} <init|status|check|draft|propose|live|harness> [options]
|
|
57
|
+
|
|
58
|
+
The repositories of one product, named in ${Workspace_1.WORKSPACE_FILE} (ADR-0041).
|
|
59
|
+
Inside a repository REQ-012 is the id; across the workspace it is fleet:REQ-012.
|
|
60
|
+
|
|
61
|
+
init [--name <n>] [--repo <name>=<path>[:<role>]]… [--force]
|
|
62
|
+
write ${Workspace_1.WORKSPACE_FILE}; without --repo, every sibling
|
|
63
|
+
folder that holds a Specgate project (roles: ${Workspace_1.WORKSPACE_ROLES.join(", ")})
|
|
64
|
+
status each repository (requirements done, branch, last gate) and each story
|
|
65
|
+
check each repository's own check, then the qualified ids and the
|
|
66
|
+
dependencies between repositories; non-zero when any fails
|
|
67
|
+
draft [<story>] [--agent "<cmd>"]
|
|
68
|
+
the client's brief below a front matter naming its stories; with a
|
|
69
|
+
story, its story.md — an agent drafts it, confined to the story
|
|
70
|
+
propose <story> [--repos <a,b,…>] [--title "<title>"]
|
|
71
|
+
a business story, and a change for it in each repository named
|
|
72
|
+
(the brief's repos when --repos is not given), carrying the story
|
|
73
|
+
live [--port <n>] every repository's harness and every story, live (studio /live);
|
|
74
|
+
--json prints the model once
|
|
75
|
+
harness run --story <id> [--dry-run] [-- <harness run options>]
|
|
76
|
+
one harness per repository with an open part of the story, in the
|
|
77
|
+
order their dependencies ask; none when a base already fails
|
|
78
|
+
|
|
79
|
+
Options: --project-dir <workspace dir>, --json.
|
|
80
|
+
`);
|
|
81
|
+
}
|
|
82
|
+
class WorkspaceCommand extends command_1.BaseCommand {
|
|
83
|
+
execute() {
|
|
84
|
+
const argv = this.args;
|
|
85
|
+
if (argv.length === 0 || argv.includes("--help") || argv.includes("-h")) {
|
|
86
|
+
usage();
|
|
87
|
+
process.exit(argv.length === 0 ? 2 : 0);
|
|
88
|
+
}
|
|
89
|
+
const json = (0, agent_1.wantsJson)(argv);
|
|
90
|
+
const io = (0, agent_1.agentIo)(json);
|
|
91
|
+
const sub = argv[0];
|
|
92
|
+
if (sub === "live" || sub === "harness") {
|
|
93
|
+
runLiveOrHarness(argv, json, io);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
let dirArg = ".";
|
|
97
|
+
let name = "";
|
|
98
|
+
let title = "";
|
|
99
|
+
let force = false;
|
|
100
|
+
const repoArgs = [];
|
|
101
|
+
let repos = "";
|
|
102
|
+
let agent = "";
|
|
103
|
+
const positional = [];
|
|
104
|
+
for (let i = 1; i < argv.length; i += 1) {
|
|
105
|
+
const a = argv[i];
|
|
106
|
+
if (a === "--project-dir")
|
|
107
|
+
dirArg = argv[++i] || ".";
|
|
108
|
+
else if (a === "--name")
|
|
109
|
+
name = argv[++i] || "";
|
|
110
|
+
else if (a === "--repo")
|
|
111
|
+
repoArgs.push(argv[++i] || "");
|
|
112
|
+
else if (a === "--repos")
|
|
113
|
+
repos = argv[++i] || "";
|
|
114
|
+
else if (a === "--title")
|
|
115
|
+
title = argv[++i] || "";
|
|
116
|
+
else if (a === "--agent")
|
|
117
|
+
agent = argv[++i] || "";
|
|
118
|
+
else if (a === "--force")
|
|
119
|
+
force = true;
|
|
120
|
+
else if (a === "--json")
|
|
121
|
+
continue;
|
|
122
|
+
else if (!a.startsWith("-"))
|
|
123
|
+
positional.push(a);
|
|
124
|
+
else
|
|
125
|
+
io.usage(NULL_SHAPE, [
|
|
126
|
+
(0, diagnostics_1.error)("unknown_flag", `Unknown argument for workspace: ${a}`, {
|
|
127
|
+
fix: "specgate workspace --help",
|
|
128
|
+
}),
|
|
129
|
+
]);
|
|
130
|
+
}
|
|
131
|
+
if (!["init", "status", "check", "draft", "propose"].includes(sub)) {
|
|
132
|
+
io.usage(NULL_SHAPE, [
|
|
133
|
+
(0, diagnostics_1.error)("unknown_subcommand", `workspace has no ${sub}.`, {
|
|
134
|
+
fix: "specgate workspace --help",
|
|
135
|
+
}),
|
|
136
|
+
]);
|
|
137
|
+
}
|
|
138
|
+
const dir = path.resolve(dirArg);
|
|
139
|
+
if (sub === "init") {
|
|
140
|
+
// REQ-290: the workspace file, from --repo or from the sibling projects.
|
|
141
|
+
const file = path.join(dir, Workspace_1.WORKSPACE_FILE);
|
|
142
|
+
if (fs.existsSync(file) && !force) {
|
|
143
|
+
io.fail(NULL_SHAPE, [
|
|
144
|
+
(0, diagnostics_1.error)("workspace_exists", `${Workspace_1.WORKSPACE_FILE} already exists.`, {
|
|
145
|
+
fix: "specgate workspace init --force writes it again.",
|
|
146
|
+
}),
|
|
147
|
+
]);
|
|
148
|
+
}
|
|
149
|
+
const members = [];
|
|
150
|
+
const problems = [];
|
|
151
|
+
for (const spec of repoArgs) {
|
|
152
|
+
const m = /^([^=]+)=([^:]+(?::[\\/][^:]*)?)(?::([a-z]+))?$/.exec(spec);
|
|
153
|
+
const repoName = m ? m[1].trim() : "";
|
|
154
|
+
const role = ((m && m[3]) || "service");
|
|
155
|
+
if (!m || !Workspace_1.REPO_NAME.test(repoName)) {
|
|
156
|
+
problems.push((0, diagnostics_1.error)("workspace_repo_arg", `--repo ${spec}: write <name>=<path>[:<role>], as fleet=../fleet.`, {
|
|
157
|
+
fix: "specgate workspace init --repo fleet=../fleet --repo web=../web:web",
|
|
158
|
+
}));
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (!Workspace_1.WORKSPACE_ROLES.includes(role)) {
|
|
162
|
+
problems.push((0, diagnostics_1.error)("workspace_role", `--repo ${spec}: the role is ${Workspace_1.WORKSPACE_ROLES.join(", ")}.`, {
|
|
163
|
+
fix: `specgate workspace init --repo ${repoName}=${m[2]}:service`,
|
|
164
|
+
}));
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
members.push({
|
|
168
|
+
name: repoName,
|
|
169
|
+
path: m[2].trim(),
|
|
170
|
+
role,
|
|
171
|
+
capabilities: capabilitiesOf(path.resolve(dir, m[2].trim())),
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
if (problems.length)
|
|
175
|
+
io.usage(NULL_SHAPE, problems);
|
|
176
|
+
if (repoArgs.length === 0)
|
|
177
|
+
members.push(...siblings(dir));
|
|
178
|
+
if (members.length === 0)
|
|
179
|
+
io.fail(NULL_SHAPE, [(0, workspace_1.noteNoMembers)()]);
|
|
180
|
+
const config = { name: name || path.basename(dir), repos: members };
|
|
181
|
+
fs.writeFileSync(file, (0, Workspace_1.renderWorkspace)(config), "utf8");
|
|
182
|
+
(0, sonar_local_1.ensureIgnored)(dir, workspace_1.LAST_CHECK_FILE);
|
|
183
|
+
// REQ-349: each repository's log of a story's build is not committed.
|
|
184
|
+
(0, sonar_local_1.ensureIgnored)(dir, `${workspace_1.RUNS_DIR}/`);
|
|
185
|
+
io.emit({ workspace: { file: Workspace_1.WORKSPACE_FILE, ...config } }, () => {
|
|
186
|
+
process.stdout.write(ui.header("workspace init", config.name));
|
|
187
|
+
// REQ-348: name, path and role in their own columns.
|
|
188
|
+
process.stdout.write(ui.table(members.map((r) => [r.name, r.path, r.role])) + "\n");
|
|
189
|
+
process.stdout.write(`\n ${ui.next("specgate workspace status", "each repository and each story")}\n`);
|
|
190
|
+
});
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const { ws, problems } = (0, workspace_1.loadWorkspace)(dir);
|
|
194
|
+
if (!ws)
|
|
195
|
+
io.fail(NULL_SHAPE, problems);
|
|
196
|
+
if (sub === "status") {
|
|
197
|
+
const model = (0, workspace_1.workspaceModel)(ws);
|
|
198
|
+
io.emit({ workspace: model }, () => {
|
|
199
|
+
process.stdout.write(ui.header("workspace status", model.name));
|
|
200
|
+
process.stdout.write(ui.section("Repositories"));
|
|
201
|
+
const rows = model.repos.map((r) => [
|
|
202
|
+
r.name,
|
|
203
|
+
r.role,
|
|
204
|
+
r.exists
|
|
205
|
+
? r.requirements
|
|
206
|
+
? `${r.requirements.done} of ${r.requirements.total} done`
|
|
207
|
+
: "no status"
|
|
208
|
+
: "missing",
|
|
209
|
+
r.branch || "-",
|
|
210
|
+
r.gate.state,
|
|
211
|
+
]);
|
|
212
|
+
process.stdout.write(ui.table(rows) + "\n");
|
|
213
|
+
if (model.stories.length) {
|
|
214
|
+
process.stdout.write(ui.section("Stories"));
|
|
215
|
+
for (const s of model.stories) {
|
|
216
|
+
const parts = s.subchanges.map((sc) => `${sc.repo} ${sc.state}`).join(" · ");
|
|
217
|
+
process.stdout.write(` ${ui.state(s.done ? "done" : "doing")} ${s.id} ${ui_1.c.muted}${parts}${ui_1.c.reset}\n`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
process.stdout.write(`\n ${ui.next("specgate workspace check", "every repository's gate, and the links between them")}\n`);
|
|
221
|
+
});
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (sub === "draft") {
|
|
225
|
+
// REQ-339..340: the client's brief, its stories, and one story drafted.
|
|
226
|
+
const brief = (0, workspace_draft_1.workspaceBrief)(ws);
|
|
227
|
+
const id = positional[0] || "";
|
|
228
|
+
if (!id) {
|
|
229
|
+
io.emit({ workspace: brief }, () => {
|
|
230
|
+
process.stdout.write(ui.header("workspace draft", ws.config.name));
|
|
231
|
+
if (brief.created)
|
|
232
|
+
process.stdout.write(ui.fileLine("added", `${brief.file} the client's brief, below a front matter`) + "\n");
|
|
233
|
+
if (brief.stories.length === 0) {
|
|
234
|
+
process.stdout.write(`\n Name the stories of the brief in ${brief.file}'s front matter:\n` +
|
|
235
|
+
` ${ui_1.c.muted}stories:\n - { id: follow-shipment, title: The client follows a shipment, repos: [${ws.config.repos
|
|
236
|
+
.slice(0, 2)
|
|
237
|
+
.map((r) => r.name)
|
|
238
|
+
.join(", ")}] }${ui_1.c.reset}\n` +
|
|
239
|
+
`\n ${ui.next("specgate workspace draft", "again, once they are named")}\n`);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
process.stdout.write(ui.section("Stories"));
|
|
243
|
+
process.stdout.write(ui.table(brief.stories.map((s) => [s.id, s.title, s.repos.join(", ")])) + "\n");
|
|
244
|
+
process.stdout.write(`\n ${ui.next(`specgate workspace draft ${brief.stories[0].id} --agent "claude -p"`, "its intent and criteria, from the brief")}\n`);
|
|
245
|
+
});
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
const story = brief.stories.find((s) => s.id === id);
|
|
249
|
+
if (!story) {
|
|
250
|
+
io.usage(NULL_SHAPE, [
|
|
251
|
+
(0, diagnostics_1.error)("workspace_story_unnamed", `${brief.file} names no story ${id}.`, {
|
|
252
|
+
fix: `Add - { id: ${id}, title: …, repos: [${ws.config.repos.map((r) => r.name).join(", ")}] } under stories: in ${brief.file}.`,
|
|
253
|
+
}),
|
|
254
|
+
]);
|
|
255
|
+
}
|
|
256
|
+
const unknownRepos = story.repos.filter((r) => !ws.dirs[r]);
|
|
257
|
+
if (unknownRepos.length) {
|
|
258
|
+
io.usage(NULL_SHAPE, [
|
|
259
|
+
(0, diagnostics_1.error)("workspace_story_repos", `Not repos of this workspace: ${unknownRepos.join(", ")}.`, {
|
|
260
|
+
fix: `repos: [${ws.config.repos.map((r) => r.name).join(", ")}]`,
|
|
261
|
+
}),
|
|
262
|
+
]);
|
|
263
|
+
}
|
|
264
|
+
const written = (0, workspace_draft_1.scaffoldStory)(ws, story);
|
|
265
|
+
let ran = null;
|
|
266
|
+
if (agent) {
|
|
267
|
+
ran = (0, workspace_draft_1.draftStoryWithAgent)(ws, story, agent, !json);
|
|
268
|
+
if (!ran.ok) {
|
|
269
|
+
io.fail(NULL_SHAPE, [
|
|
270
|
+
(0, diagnostics_1.error)("workspace_draft_agent", ran.problem === "no_git"
|
|
271
|
+
? "The workspace repository is not a git repository: the agent's writes cannot be confined."
|
|
272
|
+
: `The agent failed: ${ran.detail || "no detail"}`, {
|
|
273
|
+
fix: ran.problem === "no_git"
|
|
274
|
+
? "git init"
|
|
275
|
+
: "Run the agent command by hand to see why.",
|
|
276
|
+
}),
|
|
277
|
+
]);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
io.emit({
|
|
281
|
+
workspace: {
|
|
282
|
+
story: story,
|
|
283
|
+
written,
|
|
284
|
+
agent: ran ? { wrote: ran.wrote, reverted: ran.reverted } : null,
|
|
285
|
+
},
|
|
286
|
+
}, () => {
|
|
287
|
+
process.stdout.write(ui.header("workspace draft", story.id));
|
|
288
|
+
for (const f of written)
|
|
289
|
+
process.stdout.write(ui.fileLine("added", f) + "\n");
|
|
290
|
+
if (ran) {
|
|
291
|
+
if (ran.permissionAdded)
|
|
292
|
+
process.stdout.write(` ${ui_1.c.muted}edit permission added: the agent writes only ${(0, workspace_draft_1.storyRel)(story.id)}/${ui_1.c.reset}\n`);
|
|
293
|
+
for (const f of ran.reverted)
|
|
294
|
+
process.stdout.write(` ${ui_1.c.warn}▲${ui_1.c.reset} undone: ${f}, outside the story\n`);
|
|
295
|
+
}
|
|
296
|
+
process.stdout.write(`\n ${ui.next(`specgate workspace propose ${story.id}`, `review ${(0, workspace_draft_1.storyRel)(story.id)}/story.md, then open its change in ${story.repos.join(", ")}`)}\n`);
|
|
297
|
+
});
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
if (sub === "check") {
|
|
301
|
+
const result = (0, workspace_1.checkWorkspace)(ws);
|
|
302
|
+
(0, workspace_1.recordCheck)(ws.dir, result);
|
|
303
|
+
const diags = [
|
|
304
|
+
...result.repos.flatMap((r) => r.ok
|
|
305
|
+
? []
|
|
306
|
+
: r.problems.length
|
|
307
|
+
? r.problems.map((p) => ({ ...p, message: `${r.name}: ${p.message}` }))
|
|
308
|
+
: [
|
|
309
|
+
(0, diagnostics_1.error)("workspace_repo_gate", `${r.name}: specgate check fails.`, {
|
|
310
|
+
target: r.name,
|
|
311
|
+
fix: `Run specgate check in ${r.name}.`,
|
|
312
|
+
}),
|
|
313
|
+
]),
|
|
314
|
+
...result.problems,
|
|
315
|
+
];
|
|
316
|
+
const payload = {
|
|
317
|
+
workspace: {
|
|
318
|
+
ok: result.ok,
|
|
319
|
+
repos: result.repos.map((r) => ({ name: r.name, ok: r.ok })),
|
|
320
|
+
warnings: result.warnings,
|
|
321
|
+
},
|
|
322
|
+
};
|
|
323
|
+
if (!result.ok) {
|
|
324
|
+
if (json)
|
|
325
|
+
io.fail(payload, diags);
|
|
326
|
+
process.stdout.write(ui.header("workspace check", ws.config.name));
|
|
327
|
+
for (const r of result.repos)
|
|
328
|
+
process.stdout.write(` ${ui.state(r.ok ? "done" : "blocked")} ${r.name}\n`);
|
|
329
|
+
(0, diagnostics_1.printDiagnostics)(diags);
|
|
330
|
+
process.exit(1);
|
|
331
|
+
}
|
|
332
|
+
io.emit(payload, () => {
|
|
333
|
+
process.stdout.write(ui.header("workspace check", ws.config.name));
|
|
334
|
+
for (const r of result.repos)
|
|
335
|
+
process.stdout.write(` ${ui.state("done")} ${r.name}\n`);
|
|
336
|
+
if (result.warnings.length)
|
|
337
|
+
(0, diagnostics_1.printDiagnostics)(result.warnings);
|
|
338
|
+
process.stdout.write(`\n ${ui_1.c.ok}✔${ui_1.c.reset} Every repository passes, and the links between them hold.\n`);
|
|
339
|
+
});
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
// propose — REQ-296
|
|
343
|
+
const story = positional[0] || "";
|
|
344
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(story)) {
|
|
345
|
+
io.usage(NULL_SHAPE, [
|
|
346
|
+
(0, diagnostics_1.error)("workspace_story_id", "propose needs a story id: lower-case words joined by dashes.", {
|
|
347
|
+
fix: "specgate workspace propose follow-shipment --repos fleet,tracking,web",
|
|
348
|
+
}),
|
|
349
|
+
]);
|
|
350
|
+
}
|
|
351
|
+
// REQ-342: a story drafted from the brief brings its repositories and title.
|
|
352
|
+
const fromBrief = (0, workspace_draft_1.readWorkspaceStories)(ws).find((s) => s.id === story);
|
|
353
|
+
const names = (repos ? repos.split(",") : fromBrief ? fromBrief.repos : [])
|
|
354
|
+
.map((s) => s.trim())
|
|
355
|
+
.filter(Boolean);
|
|
356
|
+
const unknown = names.filter((n) => !ws.dirs[n]);
|
|
357
|
+
if (names.length === 0 || unknown.length) {
|
|
358
|
+
io.usage(NULL_SHAPE, [
|
|
359
|
+
(0, diagnostics_1.error)("workspace_story_repos", names.length
|
|
360
|
+
? `Not repos of this workspace: ${unknown.join(", ")}.`
|
|
361
|
+
: "propose needs --repos.", { fix: `--repos ${ws.config.repos.map((r) => r.name).join(",")}` }),
|
|
362
|
+
]);
|
|
363
|
+
}
|
|
364
|
+
const storyDir = path.join(ws.dir, Workspace_1.STORIES_DIR, story);
|
|
365
|
+
const existing = (0, workspace_1.readStories)(ws.dir).find((s) => s.id === story);
|
|
366
|
+
if (existing && existing.subchanges.length > 0) {
|
|
367
|
+
io.fail(NULL_SHAPE, [
|
|
368
|
+
(0, diagnostics_1.error)("workspace_story_exists", `The story ${story} already exists.`, {
|
|
369
|
+
fix: "specgate workspace status",
|
|
370
|
+
}),
|
|
371
|
+
]);
|
|
372
|
+
}
|
|
373
|
+
const busy = names.filter((n) => fs.existsSync(path.join(ws.dirs[n], "docs", "specs", "changes", story)));
|
|
374
|
+
if (busy.length) {
|
|
375
|
+
io.fail(NULL_SHAPE, [
|
|
376
|
+
(0, diagnostics_1.error)("workspace_change_exists", `A change ${story} is already open in ${busy.join(", ")}.`, {
|
|
377
|
+
fix: "Choose another story id, or archive that change first.",
|
|
378
|
+
}),
|
|
379
|
+
]);
|
|
380
|
+
}
|
|
381
|
+
const storyTitle = title ||
|
|
382
|
+
(existing && existing.title) ||
|
|
383
|
+
(fromBrief && fromBrief.title) ||
|
|
384
|
+
story.replace(/-/g, " ");
|
|
385
|
+
const storyMd = path.join(storyDir, "story.md");
|
|
386
|
+
const storyText = fs.existsSync(storyMd) ? fs.readFileSync(storyMd, "utf8") : "";
|
|
387
|
+
const failed = [];
|
|
388
|
+
for (const n of names) {
|
|
389
|
+
const p = (0, workspace_1.openSubChange)(ws, n, story, storyTitle, { storyText, repos: names });
|
|
390
|
+
if (p)
|
|
391
|
+
failed.push(p);
|
|
392
|
+
}
|
|
393
|
+
if (failed.length)
|
|
394
|
+
io.fail(NULL_SHAPE, failed);
|
|
395
|
+
fs.mkdirSync(storyDir, { recursive: true });
|
|
396
|
+
const record = {
|
|
397
|
+
id: story,
|
|
398
|
+
title: storyTitle,
|
|
399
|
+
subchanges: names.map((n) => ({ repo: n, change: story })),
|
|
400
|
+
};
|
|
401
|
+
fs.writeFileSync(path.join(storyDir, "story.yaml"), (0, Workspace_1.renderStory)(record), "utf8");
|
|
402
|
+
if (!storyText) {
|
|
403
|
+
fs.writeFileSync(storyMd, `# Story: ${storyTitle}\n\n## Intent\n\n<!-- The business story, in the words of whoever asked for it. It stays as it is while each repository's change works out the how. -->\n\n## Repositories\n\n` +
|
|
404
|
+
names.map((n) => `- \`${n}\`: \`docs/specs/changes/${story}/\``).join("\n") +
|
|
405
|
+
"\n", "utf8");
|
|
406
|
+
}
|
|
407
|
+
io.emit({ workspace: { story: record } }, () => {
|
|
408
|
+
process.stdout.write(ui.header("workspace propose", story));
|
|
409
|
+
for (const n of names)
|
|
410
|
+
process.stdout.write(ui.fileLine("added", `${n}/docs/specs/changes/${story}/`) + "\n");
|
|
411
|
+
process.stdout.write(ui.fileLine("added", `${Workspace_1.STORIES_DIR}/${story}/story.md`) + "\n");
|
|
412
|
+
process.stdout.write(`\n ${ui.next("specgate workspace status", "the story is done when every repository archives its change")}\n`);
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
exports.WorkspaceCommand = WorkspaceCommand;
|
|
417
|
+
/** The capabilities a member specifies: its `docs/specs/capabilities/*`. */
|
|
418
|
+
function capabilitiesOf(dir) {
|
|
419
|
+
const caps = path.join(dir, "docs", "specs", "capabilities");
|
|
420
|
+
if (!fs.existsSync(caps))
|
|
421
|
+
return [];
|
|
422
|
+
return fs
|
|
423
|
+
.readdirSync(caps, { withFileTypes: true })
|
|
424
|
+
.filter((e) => e.isDirectory())
|
|
425
|
+
.map((e) => e.name)
|
|
426
|
+
.sort();
|
|
427
|
+
}
|
|
428
|
+
/** Every sibling folder that holds a Specgate project, as a member. */
|
|
429
|
+
function siblings(dir) {
|
|
430
|
+
const parent = path.dirname(dir);
|
|
431
|
+
const out = [];
|
|
432
|
+
for (const e of fs.readdirSync(parent, { withFileTypes: true })) {
|
|
433
|
+
if (!e.isDirectory())
|
|
434
|
+
continue;
|
|
435
|
+
const full = path.join(parent, e.name);
|
|
436
|
+
if (path.resolve(full) === path.resolve(dir))
|
|
437
|
+
continue;
|
|
438
|
+
if (!fs.existsSync(path.join(full, "spec.md")))
|
|
439
|
+
continue;
|
|
440
|
+
const repoName = e.name
|
|
441
|
+
.toLowerCase()
|
|
442
|
+
.replace(/[^a-z0-9-]+/g, "-")
|
|
443
|
+
.replace(/^[^a-z]+/, "");
|
|
444
|
+
if (!Workspace_1.REPO_NAME.test(repoName))
|
|
445
|
+
continue;
|
|
446
|
+
const role = /contracts?$/.test(repoName)
|
|
447
|
+
? "contracts"
|
|
448
|
+
: /web$|frontend$/.test(repoName)
|
|
449
|
+
? "web"
|
|
450
|
+
: "service";
|
|
451
|
+
out.push({ name: repoName, path: `../${e.name}`, role, capabilities: capabilitiesOf(full) });
|
|
452
|
+
}
|
|
453
|
+
// REQ-348: truck-flote-fleet and truck-flote-web are fleet and web.
|
|
454
|
+
const short = (0, WorkspaceStory_1.shortNames)(out.map((r) => r.name));
|
|
455
|
+
out.forEach((r, i) => (r.name = short[i]));
|
|
456
|
+
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
457
|
+
}
|
|
458
|
+
/** The value after a flag, or "". */
|
|
459
|
+
function flag(argv, name) {
|
|
460
|
+
const i = argv.indexOf(name);
|
|
461
|
+
return i >= 0 ? argv[i + 1] || "" : "";
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* `workspace live` (REQ-323) and `workspace harness run --story` (REQ-324):
|
|
465
|
+
* the workspace's live view, and the build that gives it something to show.
|
|
466
|
+
*/
|
|
467
|
+
function runLiveOrHarness(argv, json, io) {
|
|
468
|
+
const sep = argv.indexOf("--");
|
|
469
|
+
const own = sep >= 0 ? argv.slice(0, sep) : argv;
|
|
470
|
+
const passthrough = sep >= 0 ? argv.slice(sep + 1) : [];
|
|
471
|
+
const dir = path.resolve(flag(own, "--project-dir") || ".");
|
|
472
|
+
const { ws, problems } = (0, workspace_1.loadWorkspace)(dir);
|
|
473
|
+
if (!ws)
|
|
474
|
+
io.fail(NULL_SHAPE, problems);
|
|
475
|
+
const cli = path.join((0, project_root_1.findCliRoot)(__dirname), "bin", "specgate.js");
|
|
476
|
+
if (own[0] === "live") {
|
|
477
|
+
if (json) {
|
|
478
|
+
io.emit({ live: (0, workspace_live_1.readWorkspaceLive)(ws) }, () => undefined);
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
// The studio serves the workspace's /live when it runs in its repository.
|
|
482
|
+
const port = flag(own, "--port");
|
|
483
|
+
const r = (0, node_child_process_1.spawnSync)(process.execPath, [cli, "studio", "--project-dir", ws.dir, ...(port ? ["--port", port] : [])], { stdio: "inherit" });
|
|
484
|
+
process.exit(r.status ?? 1);
|
|
485
|
+
}
|
|
486
|
+
if (own[1] !== "run") {
|
|
487
|
+
io.usage(NULL_SHAPE, [
|
|
488
|
+
(0, diagnostics_1.error)("unknown_subcommand", "workspace harness has run only.", {
|
|
489
|
+
fix: "specgate workspace harness run --story <id>",
|
|
490
|
+
}),
|
|
491
|
+
]);
|
|
492
|
+
}
|
|
493
|
+
const storyId = flag(own, "--story");
|
|
494
|
+
const story = (0, workspace_1.readStories)(ws.dir).find((s) => s.id === storyId);
|
|
495
|
+
if (!story) {
|
|
496
|
+
io.usage(NULL_SHAPE, [
|
|
497
|
+
(0, diagnostics_1.error)("workspace_story_unknown", `No story ${storyId || "(none given)"} in this workspace.`, {
|
|
498
|
+
fix: "specgate workspace status lists the stories.",
|
|
499
|
+
}),
|
|
500
|
+
]);
|
|
501
|
+
}
|
|
502
|
+
const runs = [];
|
|
503
|
+
// REQ-345: a repository waiting on another's part runs after it.
|
|
504
|
+
const levels = (0, workspace_1.storyLevels)(ws, story);
|
|
505
|
+
const logs = path.join(ws.dir, workspace_1.RUNS_DIR, story.id);
|
|
506
|
+
const red = [];
|
|
507
|
+
for (const sc of story.subchanges) {
|
|
508
|
+
const memberDir = ws.dirs[sc.repo];
|
|
509
|
+
if (!memberDir || !fs.existsSync(memberDir))
|
|
510
|
+
continue;
|
|
511
|
+
if ((0, workspace_1.subChangeState)(memberDir, sc.change) !== "open")
|
|
512
|
+
continue;
|
|
513
|
+
const reqs = (0, workspace_live_1.partRequirements)(memberDir, sc.change);
|
|
514
|
+
if (reqs.length === 0)
|
|
515
|
+
continue;
|
|
516
|
+
const ids = reqs.map((r) => r.id);
|
|
517
|
+
// REQ-175, judged as the member's own harness judges it: a base red outside
|
|
518
|
+
// this part fails every attempt, so no repository starts.
|
|
519
|
+
const owned = new Set([...ids, ...reqs.flatMap((r) => r.scenarios)]);
|
|
520
|
+
const problem = (0, RunCommand_1.baselineProblem)(memberDir, owned);
|
|
521
|
+
if (problem) {
|
|
522
|
+
red.push((0, diagnostics_1.error)("workspace_base_red", `${sc.repo}: ${problem.split("\n")[0]}`, {
|
|
523
|
+
target: sc.repo,
|
|
524
|
+
fix: `Run specgate check in ${sc.repo}, and make its base pass first.`,
|
|
525
|
+
}));
|
|
526
|
+
}
|
|
527
|
+
const args = [
|
|
528
|
+
"harness",
|
|
529
|
+
"run",
|
|
530
|
+
"--project-dir",
|
|
531
|
+
memberDir,
|
|
532
|
+
...ids.flatMap((id) => ["--req", id]),
|
|
533
|
+
...passthrough,
|
|
534
|
+
];
|
|
535
|
+
// REQ-344: what the other repositories of the story build, for the prompt.
|
|
536
|
+
const context = path.join(logs, `${sc.repo}.context.md`);
|
|
537
|
+
runs.push({
|
|
538
|
+
repo: sc.repo,
|
|
539
|
+
dir: memberDir,
|
|
540
|
+
requirements: ids,
|
|
541
|
+
args,
|
|
542
|
+
command: `specgate ${args.map((a) => (/\s/.test(a) ? JSON.stringify(a) : a)).join(" ")}`,
|
|
543
|
+
level: levels[sc.repo] || 1,
|
|
544
|
+
context: path.relative(ws.dir, context).split(path.sep).join("/"),
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
if (red.length)
|
|
548
|
+
io.fail({ harness: null }, red);
|
|
549
|
+
fs.mkdirSync(logs, { recursive: true });
|
|
550
|
+
for (const r of runs)
|
|
551
|
+
fs.writeFileSync(path.join(ws.dir, r.context), (0, workspace_1.storyContext)(ws, story, r.repo), "utf8");
|
|
552
|
+
const payload = {
|
|
553
|
+
harness: { story: story.id, runs: runs.map(({ dir: _d, args: _a, ...r }) => r) },
|
|
554
|
+
};
|
|
555
|
+
runs.sort((a, b) => a.level - b.level || a.repo.localeCompare(b.repo));
|
|
556
|
+
if (own.includes("--dry-run")) {
|
|
557
|
+
io.emit(payload, () => {
|
|
558
|
+
process.stdout.write(ui.header("workspace harness run", `${story.id} · dry run`));
|
|
559
|
+
for (const r of runs)
|
|
560
|
+
process.stdout.write(` ${r.level} ${r.repo} ${ui_1.c.muted}${r.command}${ui_1.c.reset}\n`);
|
|
561
|
+
});
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
if (runs.length === 0) {
|
|
565
|
+
io.emit(payload, () => process.stdout.write(" Nothing open to build in this story.\n"));
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
// One process per repository; the repositories of a level at once, each
|
|
569
|
+
// level once the one before has finished (REQ-345).
|
|
570
|
+
fs.mkdirSync(logs, { recursive: true });
|
|
571
|
+
if (!json)
|
|
572
|
+
process.stdout.write(ui.header("workspace harness run", story.id));
|
|
573
|
+
let worst = 0;
|
|
574
|
+
const results = [];
|
|
575
|
+
const byLevel = [...new Set(runs.map((r) => r.level))].sort((a, b) => a - b);
|
|
576
|
+
const runLevel = (i) => {
|
|
577
|
+
if (i >= byLevel.length) {
|
|
578
|
+
if (json)
|
|
579
|
+
process.stdout.write(JSON.stringify({ harness: { story: story.id, results } }) + "\n");
|
|
580
|
+
process.exit(worst);
|
|
581
|
+
}
|
|
582
|
+
const now = runs.filter((r) => r.level === byLevel[i]);
|
|
583
|
+
let left = now.length;
|
|
584
|
+
for (const r of now) {
|
|
585
|
+
const log = path.join(logs, `${r.repo}.log`);
|
|
586
|
+
const fd = fs.openSync(log, "w");
|
|
587
|
+
const child = (0, node_child_process_1.spawn)(process.execPath, [cli, ...r.args], {
|
|
588
|
+
stdio: ["ignore", fd, fd],
|
|
589
|
+
env: {
|
|
590
|
+
...process.env,
|
|
591
|
+
NO_COLOR: "1",
|
|
592
|
+
SPECGATE_STORY_CONTEXT: path.join(ws.dir, r.context),
|
|
593
|
+
},
|
|
594
|
+
});
|
|
595
|
+
if (!json)
|
|
596
|
+
process.stdout.write(` ${r.level} ${r.repo} started · ${path.relative(ws.dir, log)}\n`);
|
|
597
|
+
child.on("close", (code) => {
|
|
598
|
+
fs.closeSync(fd);
|
|
599
|
+
const exit = code ?? 1;
|
|
600
|
+
worst = Math.max(worst, exit);
|
|
601
|
+
results.push({ repo: r.repo, exit, log });
|
|
602
|
+
if (!json)
|
|
603
|
+
process.stdout.write(` ${ui.state(exit === 0 ? "done" : "blocked")} ${r.repo}\n`);
|
|
604
|
+
left -= 1;
|
|
605
|
+
if (left === 0)
|
|
606
|
+
runLevel(i + 1);
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
};
|
|
610
|
+
runLevel(0);
|
|
611
|
+
}
|
|
612
|
+
//# sourceMappingURL=WorkspaceCommand.js.map
|