rigjs 4.0.16 → 4.0.18
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/built/index.js +187 -180
- package/lib/classes/cicd/Deploy/CDN.ts +2 -1
- package/lib/crew/index.ts +31 -0
- package/lib/crew/pending.ts +388 -0
- package/lib/crew/vault.ts +23 -0
- package/lib/deploy/index.ts +2 -1
- package/lib/utils/redact.test.ts +43 -0
- package/lib/utils/redact.ts +48 -0
- package/package.json +2 -2
|
@@ -4,6 +4,7 @@ import crypto from 'crypto';
|
|
|
4
4
|
import axios from 'axios';
|
|
5
5
|
import * as uuid from 'uuid';
|
|
6
6
|
import { DeployTarget } from '../CICD';
|
|
7
|
+
import { redactCdnUrl } from '@/utils/redact';
|
|
7
8
|
|
|
8
9
|
type TFlag = 'break' | 'enhance_break' | null;
|
|
9
10
|
|
|
@@ -64,7 +65,7 @@ class CDN {
|
|
|
64
65
|
});
|
|
65
66
|
|
|
66
67
|
const url = `http://cdn.ap-southeast-1.aliyuncs.com?${paramConfig}`;
|
|
67
|
-
console.log('cdn update url:', url);
|
|
68
|
+
console.log('cdn update url:', redactCdnUrl(url));
|
|
68
69
|
const res = await axios.create().get(url);
|
|
69
70
|
return res.data;
|
|
70
71
|
}
|
package/lib/crew/index.ts
CHANGED
|
@@ -8,6 +8,7 @@ import crewAsk from './ask';
|
|
|
8
8
|
import crewStub from './stub';
|
|
9
9
|
import { projectAdd, projectList, projectStatus, projectSync } from './project';
|
|
10
10
|
import { roleAdd, roleList, roleShow } from './roleCommand';
|
|
11
|
+
import { pendingAdd, pendingAnswer, pendingList, pendingRemove } from './pending';
|
|
11
12
|
|
|
12
13
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
13
14
|
export function registerCrewCommands(program: any): void {
|
|
@@ -105,6 +106,36 @@ export function registerCrewCommands(program: any): void {
|
|
|
105
106
|
.option('-c, --crew <name>', 'target crew name')
|
|
106
107
|
.action(roleShow);
|
|
107
108
|
|
|
109
|
+
const pending = crew.command('pending')
|
|
110
|
+
.description('list and manage materials the user must supply (per project)');
|
|
111
|
+
pending.command('list', { isDefault: true })
|
|
112
|
+
.description('list pending questions (default action; runs when `crew pending` is used without a subcommand)')
|
|
113
|
+
.option('--crew <name>', 'target crew name')
|
|
114
|
+
.option('-p, --project <name>', 'limit to one project')
|
|
115
|
+
.option('--all', 'include resolved questions')
|
|
116
|
+
.option('--json', 'machine-readable output')
|
|
117
|
+
.action(pendingList);
|
|
118
|
+
pending.command('add <title...>')
|
|
119
|
+
.description('record a new pending question / missing material')
|
|
120
|
+
.option('--crew <name>', 'target crew name')
|
|
121
|
+
.option('-p, --project <name>', 'project name (auto-detected from CWD if omitted)')
|
|
122
|
+
.option('--why <text>', 'why this information is needed')
|
|
123
|
+
.option('--need <text>', 'what to provide (file path, value, decision, etc.)')
|
|
124
|
+
.option('--priority <level>', 'high | medium | low')
|
|
125
|
+
.option('--asked-by <role>', 'role or person who raised the question (default: lead)')
|
|
126
|
+
.action((title: string[], opts: { crew?: string; project?: string; why?: string; need?: string; priority?: string; askedBy?: string }) => pendingAdd(title, opts));
|
|
127
|
+
pending.command('answer <id>')
|
|
128
|
+
.description('mark a pending question as resolved')
|
|
129
|
+
.option('--crew <name>', 'target crew name')
|
|
130
|
+
.option('-p, --project <name>', 'limit to one project')
|
|
131
|
+
.option('-n, --note <text>', 'short note describing what the user supplied')
|
|
132
|
+
.action(pendingAnswer);
|
|
133
|
+
pending.command('remove <id>')
|
|
134
|
+
.description('delete a pending question (use answer to keep history)')
|
|
135
|
+
.option('--crew <name>', 'target crew name')
|
|
136
|
+
.option('-p, --project <name>', 'limit to one project')
|
|
137
|
+
.action(pendingRemove);
|
|
138
|
+
|
|
108
139
|
crew.command('plan').description('planned: Lead refine + decompose').action(crewStub('plan'));
|
|
109
140
|
crew.command('refine').description('planned: update Shared/Spec.md').action(crewStub('refine'));
|
|
110
141
|
crew.command('decompose').description('planned: split Spec into owner/role tasks').action(crewStub('decompose'));
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import print from '../print';
|
|
4
|
+
import { CrewEntry, CrewProject, requireCrew, shortPath } from './config';
|
|
5
|
+
import { rootPath, writeText, readText } from './vault';
|
|
6
|
+
|
|
7
|
+
export interface PendingQuestion {
|
|
8
|
+
id: string;
|
|
9
|
+
title: string;
|
|
10
|
+
asked: string; // ISO date (YYYY-MM-DD)
|
|
11
|
+
askedBy?: string;
|
|
12
|
+
priority?: 'high' | 'medium' | 'low' | string;
|
|
13
|
+
why?: string;
|
|
14
|
+
need?: string;
|
|
15
|
+
notes?: string;
|
|
16
|
+
status: 'open' | 'resolved';
|
|
17
|
+
resolved?: string; // ISO date when resolved
|
|
18
|
+
answer?: string;
|
|
19
|
+
bodyExtras?: string; // extra free-form body lines we want to preserve
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface PendingListOpts {
|
|
23
|
+
crew?: string;
|
|
24
|
+
project?: string;
|
|
25
|
+
all?: boolean;
|
|
26
|
+
json?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface PendingAddOpts {
|
|
30
|
+
crew?: string;
|
|
31
|
+
project?: string;
|
|
32
|
+
why?: string;
|
|
33
|
+
need?: string;
|
|
34
|
+
priority?: string;
|
|
35
|
+
askedBy?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface PendingAnswerOpts {
|
|
39
|
+
crew?: string;
|
|
40
|
+
project?: string;
|
|
41
|
+
note?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface PendingRemoveOpts {
|
|
45
|
+
crew?: string;
|
|
46
|
+
project?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function pendingList(opts: PendingListOpts): void {
|
|
50
|
+
const crew = requireCrew(opts.crew);
|
|
51
|
+
const targets = resolveTargets(crew, opts.project);
|
|
52
|
+
const includeResolved = !!opts.all;
|
|
53
|
+
const result: { project: string; file: string; questions: PendingQuestion[] }[] = [];
|
|
54
|
+
for (const project of targets) {
|
|
55
|
+
const file = pendingFile(crew, project.name);
|
|
56
|
+
const questions = readQuestions(file)
|
|
57
|
+
.filter(q => includeResolved || q.status === 'open');
|
|
58
|
+
result.push({ project: project.name, file, questions });
|
|
59
|
+
}
|
|
60
|
+
if (opts.json) {
|
|
61
|
+
// eslint-disable-next-line no-console
|
|
62
|
+
console.log(JSON.stringify({ ok: true, data: result }, null, 2));
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const total = result.reduce((sum, r) => sum + r.questions.length, 0);
|
|
66
|
+
if (total === 0) {
|
|
67
|
+
print.info(includeResolved
|
|
68
|
+
? 'no pending questions found.'
|
|
69
|
+
: 'no open pending questions. Use --all to include resolved.');
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
print.info(`pending questions${includeResolved ? ' (all)' : ' (open)'}: ${total}`);
|
|
73
|
+
for (const block of result) {
|
|
74
|
+
if (block.questions.length === 0) continue;
|
|
75
|
+
print.info(`project: ${block.project} file: ${shortPath(block.file)}`);
|
|
76
|
+
for (const q of block.questions) {
|
|
77
|
+
const tag = q.status === 'resolved' ? '[resolved]' : `[${q.priority || 'open'}]`;
|
|
78
|
+
// eslint-disable-next-line no-console
|
|
79
|
+
console.log(`- ${q.id} ${tag} ${q.title}`);
|
|
80
|
+
if (q.why) {
|
|
81
|
+
// eslint-disable-next-line no-console
|
|
82
|
+
console.log(` why: ${q.why}`);
|
|
83
|
+
}
|
|
84
|
+
if (q.need) {
|
|
85
|
+
// eslint-disable-next-line no-console
|
|
86
|
+
console.log(` need: ${q.need}`);
|
|
87
|
+
}
|
|
88
|
+
if (q.answer) {
|
|
89
|
+
// eslint-disable-next-line no-console
|
|
90
|
+
console.log(` answer: ${q.answer}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function pendingAdd(titleParts: string[] | undefined, opts: PendingAddOpts): void {
|
|
97
|
+
const crew = requireCrew(opts.crew);
|
|
98
|
+
const project = resolveSingleProject(crew, opts.project, 'add');
|
|
99
|
+
const title = (titleParts || []).join(' ').trim();
|
|
100
|
+
if (!title) {
|
|
101
|
+
print.error('missing question title. Usage: rig crew pending add "<title>" --project <name>');
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
const file = pendingFile(crew, project.name);
|
|
105
|
+
const questions = readQuestions(file);
|
|
106
|
+
const id = nextQuestionId(questions);
|
|
107
|
+
const today = isoDate(new Date());
|
|
108
|
+
const question: PendingQuestion = {
|
|
109
|
+
id,
|
|
110
|
+
title,
|
|
111
|
+
asked: today,
|
|
112
|
+
askedBy: opts.askedBy,
|
|
113
|
+
priority: opts.priority,
|
|
114
|
+
why: opts.why,
|
|
115
|
+
need: opts.need,
|
|
116
|
+
status: 'open',
|
|
117
|
+
};
|
|
118
|
+
questions.push(question);
|
|
119
|
+
writeQuestions(file, project, questions);
|
|
120
|
+
print.succeed(`added ${id} to ${shortPath(file)}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function pendingAnswer(id: string, opts: PendingAnswerOpts): void {
|
|
124
|
+
const crew = requireCrew(opts.crew);
|
|
125
|
+
const located = findQuestion(crew, id, opts.project);
|
|
126
|
+
if (!located) {
|
|
127
|
+
print.error(`question not found: ${id}`);
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
const { project, questions, index } = located;
|
|
131
|
+
const question = questions[index];
|
|
132
|
+
question.status = 'resolved';
|
|
133
|
+
question.resolved = isoDate(new Date());
|
|
134
|
+
if (opts.note) question.answer = opts.note;
|
|
135
|
+
writeQuestions(pendingFile(crew, project.name), project, questions);
|
|
136
|
+
print.succeed(`resolved ${id} in project ${project.name}`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function pendingRemove(id: string, opts: PendingRemoveOpts): void {
|
|
140
|
+
const crew = requireCrew(opts.crew);
|
|
141
|
+
const located = findQuestion(crew, id, opts.project);
|
|
142
|
+
if (!located) {
|
|
143
|
+
print.error(`question not found: ${id}`);
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
const { project, questions, index } = located;
|
|
147
|
+
questions.splice(index, 1);
|
|
148
|
+
writeQuestions(pendingFile(crew, project.name), project, questions);
|
|
149
|
+
print.succeed(`removed ${id} from project ${project.name}`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function pendingFile(crew: CrewEntry, project: string): string {
|
|
153
|
+
return rootPath(crew, path.join('Projects', project, 'Pending-Questions.md'));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function resolveTargets(crew: CrewEntry, projectName?: string): CrewProject[] {
|
|
157
|
+
if (projectName) {
|
|
158
|
+
const project = (crew.projects || []).find(p => p.name === projectName);
|
|
159
|
+
if (!project) {
|
|
160
|
+
print.error(`unknown project: ${projectName}`);
|
|
161
|
+
process.exit(1);
|
|
162
|
+
}
|
|
163
|
+
return [project];
|
|
164
|
+
}
|
|
165
|
+
return crew.projects || [];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function resolveSingleProject(crew: CrewEntry, projectName: string | undefined, action: string): CrewProject {
|
|
169
|
+
const name = projectName || autoProject(crew);
|
|
170
|
+
if (!name) {
|
|
171
|
+
print.error(`cannot determine project for "${action}". Use --project <name>.`);
|
|
172
|
+
process.exit(1);
|
|
173
|
+
}
|
|
174
|
+
const project = (crew.projects || []).find(p => p.name === name);
|
|
175
|
+
if (!project) {
|
|
176
|
+
print.error(`unknown project: ${name}`);
|
|
177
|
+
process.exit(1);
|
|
178
|
+
}
|
|
179
|
+
return project;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function autoProject(crew: CrewEntry): string | undefined {
|
|
183
|
+
const cwd = process.cwd();
|
|
184
|
+
const match = (crew.projects || []).find(p => isInside(cwd, p.path));
|
|
185
|
+
return match?.name;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function findQuestion(
|
|
189
|
+
crew: CrewEntry,
|
|
190
|
+
id: string,
|
|
191
|
+
projectName?: string,
|
|
192
|
+
): { project: CrewProject; questions: PendingQuestion[]; index: number } | undefined {
|
|
193
|
+
const targets = resolveTargets(crew, projectName);
|
|
194
|
+
for (const project of targets) {
|
|
195
|
+
const file = pendingFile(crew, project.name);
|
|
196
|
+
const questions = readQuestions(file);
|
|
197
|
+
const index = questions.findIndex(q => q.id === id);
|
|
198
|
+
if (index >= 0) return { project, questions, index };
|
|
199
|
+
}
|
|
200
|
+
return undefined;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function isInside(child: string, parent: string): boolean {
|
|
204
|
+
const rel = path.relative(path.resolve(parent), path.resolve(child));
|
|
205
|
+
return rel === '' || (!!rel && !rel.startsWith('..') && !path.isAbsolute(rel));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const HEADER_LINE = '<!-- rig-crew-pending:v1 -->';
|
|
209
|
+
const OPEN_HEADING = '## Open';
|
|
210
|
+
const RESOLVED_HEADING = '## Resolved';
|
|
211
|
+
|
|
212
|
+
export function readQuestions(file: string): PendingQuestion[] {
|
|
213
|
+
if (!fs.existsSync(file)) return [];
|
|
214
|
+
const text = readText(file);
|
|
215
|
+
const lines = text.split(/\r?\n/);
|
|
216
|
+
const questions: PendingQuestion[] = [];
|
|
217
|
+
let currentStatus: 'open' | 'resolved' = 'open';
|
|
218
|
+
let buffer: string[] = [];
|
|
219
|
+
let buffering = false;
|
|
220
|
+
|
|
221
|
+
const flush = () => {
|
|
222
|
+
if (!buffering) return;
|
|
223
|
+
const parsed = parseQuestionBlock(buffer, currentStatus);
|
|
224
|
+
if (parsed) questions.push(parsed);
|
|
225
|
+
buffer = [];
|
|
226
|
+
buffering = false;
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
for (const line of lines) {
|
|
230
|
+
if (line.trim() === OPEN_HEADING) {
|
|
231
|
+
flush();
|
|
232
|
+
currentStatus = 'open';
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
if (line.trim() === RESOLVED_HEADING) {
|
|
236
|
+
flush();
|
|
237
|
+
currentStatus = 'resolved';
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
if (/^###\s+/.test(line)) {
|
|
241
|
+
flush();
|
|
242
|
+
buffering = true;
|
|
243
|
+
buffer.push(line);
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
if (buffering) buffer.push(line);
|
|
247
|
+
}
|
|
248
|
+
flush();
|
|
249
|
+
return questions;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function parseQuestionBlock(lines: string[], status: 'open' | 'resolved'): PendingQuestion | undefined {
|
|
253
|
+
if (lines.length === 0) return undefined;
|
|
254
|
+
const heading = lines[0].replace(/^###\s+/, '').trim();
|
|
255
|
+
const m = heading.match(/^([A-Z]+-\d{6}-\d{3})\s*[—-]?\s*(.*)$/);
|
|
256
|
+
if (!m) return undefined;
|
|
257
|
+
const id = m[1];
|
|
258
|
+
let title = m[2].trim();
|
|
259
|
+
const resolvedMark = title.match(/_\(resolved\s+(\d{4}-\d{2}-\d{2})\)_\s*$/);
|
|
260
|
+
let resolved: string | undefined;
|
|
261
|
+
if (resolvedMark) {
|
|
262
|
+
resolved = resolvedMark[1];
|
|
263
|
+
title = title.slice(0, resolvedMark.index).trim();
|
|
264
|
+
}
|
|
265
|
+
const question: PendingQuestion = {
|
|
266
|
+
id,
|
|
267
|
+
title,
|
|
268
|
+
asked: '',
|
|
269
|
+
status,
|
|
270
|
+
};
|
|
271
|
+
if (resolved) {
|
|
272
|
+
question.status = 'resolved';
|
|
273
|
+
question.resolved = resolved;
|
|
274
|
+
}
|
|
275
|
+
const extras: string[] = [];
|
|
276
|
+
for (let i = 1; i < lines.length; i++) {
|
|
277
|
+
const line = lines[i];
|
|
278
|
+
const bullet = line.match(/^\s*-\s+([A-Za-z][A-Za-z ]+?):\s+(.*)$/);
|
|
279
|
+
if (bullet) {
|
|
280
|
+
const key = bullet[1].toLowerCase().replace(/\s+/g, '-');
|
|
281
|
+
const value = bullet[2].trim();
|
|
282
|
+
switch (key) {
|
|
283
|
+
case 'asked':
|
|
284
|
+
question.asked = value;
|
|
285
|
+
break;
|
|
286
|
+
case 'asked-by':
|
|
287
|
+
question.askedBy = value;
|
|
288
|
+
break;
|
|
289
|
+
case 'priority':
|
|
290
|
+
question.priority = value;
|
|
291
|
+
break;
|
|
292
|
+
case 'why-needed':
|
|
293
|
+
case 'why':
|
|
294
|
+
question.why = value;
|
|
295
|
+
break;
|
|
296
|
+
case 'what-to-provide':
|
|
297
|
+
case 'need':
|
|
298
|
+
question.need = value;
|
|
299
|
+
break;
|
|
300
|
+
case 'notes':
|
|
301
|
+
question.notes = value;
|
|
302
|
+
break;
|
|
303
|
+
case 'resolved':
|
|
304
|
+
question.resolved = value;
|
|
305
|
+
question.status = 'resolved';
|
|
306
|
+
break;
|
|
307
|
+
case 'answer':
|
|
308
|
+
question.answer = value;
|
|
309
|
+
break;
|
|
310
|
+
default:
|
|
311
|
+
extras.push(line);
|
|
312
|
+
}
|
|
313
|
+
} else if (line.trim().length > 0) {
|
|
314
|
+
extras.push(line);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
if (extras.length > 0) question.bodyExtras = extras.join('\n').trim();
|
|
318
|
+
return question;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function writeQuestions(file: string, project: CrewProject, questions: PendingQuestion[]): void {
|
|
322
|
+
const open = questions.filter(q => q.status === 'open');
|
|
323
|
+
const resolved = questions.filter(q => q.status === 'resolved');
|
|
324
|
+
const sections: string[] = [
|
|
325
|
+
`# ${project.name} Pending Questions`,
|
|
326
|
+
'',
|
|
327
|
+
HEADER_LINE,
|
|
328
|
+
'',
|
|
329
|
+
'Materials / facts / decisions the user must supply before the crew can proceed.',
|
|
330
|
+
'Add with `rig crew pending add "<title>" --project <name>`; resolve with `rig crew pending answer <id> --note "..."`.',
|
|
331
|
+
'',
|
|
332
|
+
OPEN_HEADING,
|
|
333
|
+
'',
|
|
334
|
+
];
|
|
335
|
+
if (open.length === 0) {
|
|
336
|
+
sections.push('_No open questions._');
|
|
337
|
+
sections.push('');
|
|
338
|
+
} else {
|
|
339
|
+
for (const q of open) sections.push(renderQuestion(q), '');
|
|
340
|
+
}
|
|
341
|
+
sections.push(RESOLVED_HEADING, '');
|
|
342
|
+
if (resolved.length === 0) {
|
|
343
|
+
sections.push('_No resolved questions yet._');
|
|
344
|
+
sections.push('');
|
|
345
|
+
} else {
|
|
346
|
+
for (const q of resolved) sections.push(renderQuestion(q), '');
|
|
347
|
+
}
|
|
348
|
+
writeText(file, sections.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd() + '\n');
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function renderQuestion(q: PendingQuestion): string {
|
|
352
|
+
const headingExtra = q.status === 'resolved' && q.resolved ? ` _(resolved ${q.resolved})_` : '';
|
|
353
|
+
const lines: string[] = [`### ${q.id} — ${q.title}${headingExtra}`];
|
|
354
|
+
if (q.asked) lines.push(`- Asked: ${q.asked}`);
|
|
355
|
+
if (q.askedBy) lines.push(`- Asked by: ${q.askedBy}`);
|
|
356
|
+
if (q.priority) lines.push(`- Priority: ${q.priority}`);
|
|
357
|
+
if (q.why) lines.push(`- Why needed: ${q.why}`);
|
|
358
|
+
if (q.need) lines.push(`- What to provide: ${q.need}`);
|
|
359
|
+
if (q.notes) lines.push(`- Notes: ${q.notes}`);
|
|
360
|
+
if (q.status === 'resolved') {
|
|
361
|
+
if (q.resolved && !q.asked) lines.push(`- Resolved: ${q.resolved}`);
|
|
362
|
+
if (q.answer) lines.push(`- Answer: ${q.answer}`);
|
|
363
|
+
}
|
|
364
|
+
if (q.bodyExtras) lines.push('', q.bodyExtras);
|
|
365
|
+
return lines.join('\n');
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function nextQuestionId(questions: PendingQuestion[]): string {
|
|
369
|
+
const now = new Date();
|
|
370
|
+
const yy = String(now.getFullYear()).slice(-2);
|
|
371
|
+
const mm = String(now.getMonth() + 1).padStart(2, '0');
|
|
372
|
+
const dd = String(now.getDate()).padStart(2, '0');
|
|
373
|
+
const prefix = `Q-${yy}${mm}${dd}-`;
|
|
374
|
+
let max = 0;
|
|
375
|
+
for (const q of questions) {
|
|
376
|
+
if (!q.id.startsWith(prefix)) continue;
|
|
377
|
+
const suffix = parseInt(q.id.slice(prefix.length), 10);
|
|
378
|
+
if (!Number.isNaN(suffix) && suffix > max) max = suffix;
|
|
379
|
+
}
|
|
380
|
+
return `${prefix}${String(max + 1).padStart(3, '0')}`;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function isoDate(d: Date): string {
|
|
384
|
+
const yyyy = d.getFullYear();
|
|
385
|
+
const mm = String(d.getMonth() + 1).padStart(2, '0');
|
|
386
|
+
const dd = String(d.getDate()).padStart(2, '0');
|
|
387
|
+
return `${yyyy}-${mm}-${dd}`;
|
|
388
|
+
}
|
package/lib/crew/vault.ts
CHANGED
|
@@ -47,6 +47,7 @@ export function ensureProject(crew: CrewEntry, project: CrewProject): void {
|
|
|
47
47
|
writeProjectOwnerFile(path.join(base, 'Owner.md'), project);
|
|
48
48
|
writeIfMissing(path.join(base, 'Context.md'), `# ${project.name} Context\n\n`);
|
|
49
49
|
writeIfMissing(path.join(base, 'Tasks.md'), renderProjectTasksFile(project));
|
|
50
|
+
writeIfMissing(path.join(base, 'Pending-Questions.md'), renderProjectPendingFile(project));
|
|
50
51
|
ensureTasklists(base);
|
|
51
52
|
writeIfMissing(path.join(base, 'Decisions.md'), `# ${project.name} Decisions\n\n`);
|
|
52
53
|
writeIfMissing(path.join(base, 'Test-Plan.md'), `# ${project.name} Test Plan\n\n`);
|
|
@@ -185,6 +186,26 @@ function renderProjectTasksFile(project: CrewProject): string {
|
|
|
185
186
|
].join('\n');
|
|
186
187
|
}
|
|
187
188
|
|
|
189
|
+
function renderProjectPendingFile(project: CrewProject): string {
|
|
190
|
+
return [
|
|
191
|
+
`# ${project.name} Pending Questions`,
|
|
192
|
+
'',
|
|
193
|
+
'<!-- rig-crew-pending:v1 -->',
|
|
194
|
+
'',
|
|
195
|
+
'Materials / facts / decisions the user must supply before the crew can proceed.',
|
|
196
|
+
'Add with `rig crew pending add "<title>" --project <name>`; resolve with `rig crew pending answer <id> --note "..."`.',
|
|
197
|
+
'',
|
|
198
|
+
'## Open',
|
|
199
|
+
'',
|
|
200
|
+
'_No open questions._',
|
|
201
|
+
'',
|
|
202
|
+
'## Resolved',
|
|
203
|
+
'',
|
|
204
|
+
'_No resolved questions yet._',
|
|
205
|
+
'',
|
|
206
|
+
].join('\n');
|
|
207
|
+
}
|
|
208
|
+
|
|
188
209
|
function renderProjectAgentTasksFile(project: CrewProject, role: CrewRoleDefinition): string {
|
|
189
210
|
return [
|
|
190
211
|
`# ${project.name} ${role.title} Tasks`,
|
|
@@ -254,6 +275,7 @@ function renderVaultAgentInstructions(crew: CrewEntry): string {
|
|
|
254
275
|
`- Role registry: \`${root}/Shared/Roles.md\``,
|
|
255
276
|
`- Reusable role descriptions: \`${root}/<role>/Role.md\` and \`${root}/Roles/<custom-role>/Role.md\``,
|
|
256
277
|
`- Project owner memory: \`${root}/Projects/<project>/\``,
|
|
278
|
+
`- Per-project pending questions (待补充资料): \`${root}/Projects/<project>/Pending-Questions.md\` — managed by \`rig crew pending\``,
|
|
257
279
|
`- Project-scoped agent tasks: \`${root}/Projects/<project>/Agents/<role>/Tasks.md\``,
|
|
258
280
|
`- Large active task batches: \`${root}/Projects/<project>/Tasklists/active/*.md\` and \`${root}/Projects/<project>/Agents/<role>/Tasklists/active/*.md\`. Keep \`Tasks.md\` short; archived tasklists are not part of the active dashboard.`,
|
|
259
281
|
'- Vault-local scratch projects belong under `tmp/<project>/`.',
|
|
@@ -272,6 +294,7 @@ function renderVaultAgentInstructions(crew: CrewEntry): string {
|
|
|
272
294
|
'5. Treat Crew Lead as the default orchestrator prompt/protocol, not as a required Claude/Codex subagent. Subagents may be used as optional executors for specific roles, but Vault files are the source of truth.',
|
|
273
295
|
'6. Lead communicates with other roles through Markdown tasks and delegation packets, not private chat state. Use `[role:: <role>]`, `[owner:: <owner>]`, `[project:: <project>]`, `[executor:: <executor>]`, and status fields in the relevant project-scoped `Tasks.md`.',
|
|
274
296
|
`7. Worker results must be written back to the relevant role/project files under \`${root}/\`; user-facing questions go to \`${root}/Inbox.md\` for Lead to surface.`,
|
|
297
|
+
`8. Whenever a project is blocked by missing user-supplied material (API key, screenshot, vendor decision, sample data), record it with \`rig crew pending add ...\` rather than silently asking the human; when the user supplies it, the agent itself calls \`rig crew pending answer <id> --note "<summary>"\` and continues. Check \`rig crew pending --project <name>\` before starting any new tick on that project.`,
|
|
275
298
|
'',
|
|
276
299
|
AGENT_RULES_END,
|
|
277
300
|
].join('\n');
|
package/lib/deploy/index.ts
CHANGED
|
@@ -3,6 +3,7 @@ import path from 'path';
|
|
|
3
3
|
import CICD from '@/classes/cicd/CICD';
|
|
4
4
|
import CICDCmd from '@/classes/cicd/CICDCmd';
|
|
5
5
|
import AliOSS from '@/classes/cicd/Deploy/AliDeploy';
|
|
6
|
+
import { redactTarget } from '@/utils/redact';
|
|
6
7
|
|
|
7
8
|
let filesList: string[] = [];
|
|
8
9
|
const traverseFolder = (url: string) => {
|
|
@@ -33,7 +34,7 @@ export default async (cmd: any) => {
|
|
|
33
34
|
const target = Array.isArray(cicdCmd.cicd.target)
|
|
34
35
|
? cicdCmd.cicd.target[0]
|
|
35
36
|
: cicdCmd.cicd.target;
|
|
36
|
-
console.log('oss tagert', target);
|
|
37
|
+
console.log('oss tagert', redactTarget(target));
|
|
37
38
|
const aliOss = new AliOSS(target);
|
|
38
39
|
console.log('Please Wait for Upload OSS...');
|
|
39
40
|
if (!cicdCmd.endpoints || cicdCmd.endpoints.length === 0) {
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { maskSecret, redactTarget, redactCdnUrl } from '@/utils/redact';
|
|
2
|
+
|
|
3
|
+
test('maskSecret keeps a head+tail hint for long secrets', () => {
|
|
4
|
+
const ak = 'LTAI5t9GjXQc7itTohf68ZLq';
|
|
5
|
+
const masked = maskSecret(ak);
|
|
6
|
+
expect(masked).not.toContain('GjXQc7it'); // no middle bytes
|
|
7
|
+
expect(masked.startsWith('LTAI')).toBe(true);
|
|
8
|
+
expect(masked.endsWith('8ZLq')).toBe(true);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test('maskSecret returns plain mask for short / empty input', () => {
|
|
12
|
+
expect(maskSecret('')).toBe('');
|
|
13
|
+
expect(maskSecret(undefined)).toBe('');
|
|
14
|
+
expect(maskSecret('abc')).toBe('****');
|
|
15
|
+
expect(maskSecret('abcdefgh')).toBe('****');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('redactTarget masks access_key / access_secret only', () => {
|
|
19
|
+
const out = redactTarget({
|
|
20
|
+
id: 'alicloud',
|
|
21
|
+
type: 'alicloud',
|
|
22
|
+
bucket: 'my-bucket',
|
|
23
|
+
region: 'oss-ap-southeast-1',
|
|
24
|
+
access_key: 'LTAI5t9GjXQc7itTohf68ZLq',
|
|
25
|
+
access_secret: '8jfykQQoK66RldfSo9YlfdLh423GXA',
|
|
26
|
+
root_path: '/',
|
|
27
|
+
});
|
|
28
|
+
expect(out.bucket).toBe('my-bucket');
|
|
29
|
+
expect(out.region).toBe('oss-ap-southeast-1');
|
|
30
|
+
expect(out.root_path).toBe('/');
|
|
31
|
+
expect(out.access_key).not.toContain('GjXQc7it');
|
|
32
|
+
expect(out.access_secret).not.toContain('QQoK66Rld');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('redactCdnUrl masks AccessKeyId and Signature only', () => {
|
|
36
|
+
const url =
|
|
37
|
+
'http://cdn.ap-southeast-1.aliyuncs.com?AccessKeyId=LTAI5t9GjXQc7itTohf68ZLq&Action=BatchSetCdnDomainConfig&Signature=iUppVaZSIecVi3DhZZeBCf24Ag0%3D&Timestamp=2026-05-25T08%3A40%3A51.135Z';
|
|
38
|
+
const masked = redactCdnUrl(url);
|
|
39
|
+
expect(masked).not.toContain('LTAI5t9GjXQc7itTohf68ZLq');
|
|
40
|
+
expect(masked).not.toContain('iUppVaZSIecVi3DhZZeBCf24Ag0');
|
|
41
|
+
expect(masked).toContain('Action=BatchSetCdnDomainConfig');
|
|
42
|
+
expect(masked).toContain('Timestamp=2026-05-25');
|
|
43
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Helpers to keep cloud credentials out of stdout.
|
|
2
|
+
//
|
|
3
|
+
// rig's deploy/publish flow prints (a) the resolved deploy target and
|
|
4
|
+
// (b) every Aliyun CDN API URL. Both carry the AccessKeyId / AccessKeySecret
|
|
5
|
+
// in clear, which makes the console output unsafe to copy/paste into issues,
|
|
6
|
+
// CI logs, or chat.
|
|
7
|
+
//
|
|
8
|
+
// Use `maskSecret` for short identifiers (keeps a head+tail hint so two
|
|
9
|
+
// different keys are still distinguishable in logs), `redactTarget` before
|
|
10
|
+
// console-logging a DeployTarget, and `redactCdnUrl` before logging any
|
|
11
|
+
// signed Aliyun OpenAPI URL.
|
|
12
|
+
|
|
13
|
+
/** Mask a credential while keeping a short prefix + suffix for debuggability. */
|
|
14
|
+
export function maskSecret(s: string | undefined | null): string {
|
|
15
|
+
if (!s) return '';
|
|
16
|
+
if (s.length <= 8) return '****';
|
|
17
|
+
return `${s.slice(0, 4)}…${s.slice(-4)}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Return a shallow copy of a DeployTarget-shaped object with `access_key`
|
|
22
|
+
* and `access_secret` masked. Unknown keys pass through unchanged.
|
|
23
|
+
*/
|
|
24
|
+
export function redactTarget<T extends Record<string, any>>(target: T): T {
|
|
25
|
+
if (!target || typeof target !== 'object') return target;
|
|
26
|
+
const out: Record<string, any> = { ...target };
|
|
27
|
+
if (typeof out.access_key === 'string') out.access_key = maskSecret(out.access_key);
|
|
28
|
+
if (typeof out.access_secret === 'string') out.access_secret = maskSecret(out.access_secret);
|
|
29
|
+
return out as T;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Redact `AccessKeyId` and `Signature` query parameters from an Aliyun
|
|
34
|
+
* OpenAPI URL so the URL can be safely logged. Leaves all other params
|
|
35
|
+
* (Action, Timestamp, etc.) intact for debugging.
|
|
36
|
+
*/
|
|
37
|
+
export function redactCdnUrl(url: string): string {
|
|
38
|
+
if (!url) return url;
|
|
39
|
+
return url
|
|
40
|
+
.replace(/([?&]AccessKeyId=)([^&]+)/i, (_m, p1, v) => `${p1}${maskSecret(v)}`)
|
|
41
|
+
.replace(/([?&]Signature=)([^&]+)/i, (_m, p1) => `${p1}REDACTED`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export default {
|
|
45
|
+
maskSecret,
|
|
46
|
+
redactTarget,
|
|
47
|
+
redactCdnUrl,
|
|
48
|
+
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rigjs",
|
|
3
|
-
"version": "4.0.
|
|
4
|
-
"versionCode":
|
|
3
|
+
"version": "4.0.18",
|
|
4
|
+
"versionCode": 26052501,
|
|
5
5
|
"description": "A multi-repos dev tool based on yarn and git.Rigjs is intended to be the simplest way to develop,share and deliver codes between different developers or different projects.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"modular",
|