@ghl-ai/aw 0.1.44-beta.8 → 0.1.44
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/c4/index.mjs +8 -0
- package/c4/initRepo.mjs +567 -0
- package/c4/templates/claude/scripts/claude-web-bootstrap.sh +8 -0
- package/c4/templates/codex/config.toml +5 -0
- package/c4/templates/codex/scripts/codex-web-bootstrap.sh +8 -0
- package/c4/templates/cursor/environment.json +5 -0
- package/c4/templates/gitignore-block.txt +14 -0
- package/c4/templates/manifest.json +41 -0
- package/c4/templates/scripts/aw-c4-bootstrap.sh +100 -0
- package/cli.mjs +2 -0
- package/commands/init-repo.mjs +94 -0
- package/package.json +1 -1
package/c4/index.mjs
CHANGED
package/c4/initRepo.mjs
ADDED
|
@@ -0,0 +1,567 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* c4/initRepo.mjs — engine for `aw init-repo`.
|
|
3
|
+
*
|
|
4
|
+
* Scaffolds the 4 cloud-bootstrap files into a target git repo from
|
|
5
|
+
* bundled literal templates (under ./templates/). Idempotent:
|
|
6
|
+
* SHA-256 byte compare per file decides create / skip-equal /
|
|
7
|
+
* blocked-edited / overwrite. `--dry-run` and `--diff` are read-only.
|
|
8
|
+
*
|
|
9
|
+
* Public surface (composed in this order):
|
|
10
|
+
* loadManifest(templatesDir) — read & parse templates/manifest.json
|
|
11
|
+
* planActions(opts, manifest) — pure: decide per-file action
|
|
12
|
+
* applyActions(opts, actions) — FS effects (gated by dryRun)
|
|
13
|
+
* initRepo(opts) — orchestrator + exitCode summary
|
|
14
|
+
*
|
|
15
|
+
* Contract: spec.md::"New: libs/aw/c4/initRepo.mjs (engine)"
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
existsSync,
|
|
20
|
+
readFileSync,
|
|
21
|
+
writeFileSync,
|
|
22
|
+
mkdirSync,
|
|
23
|
+
chmodSync,
|
|
24
|
+
statSync,
|
|
25
|
+
} from 'node:fs';
|
|
26
|
+
import { join, dirname, basename, resolve } from 'node:path';
|
|
27
|
+
import { fileURLToPath } from 'node:url';
|
|
28
|
+
import { createHash } from 'node:crypto';
|
|
29
|
+
import { spawnSync } from 'node:child_process';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Default `git check-ignore` runner. Returns true when `relpath` is ignored
|
|
33
|
+
* by the user's gitignore rules, false otherwise (or when git is unavailable).
|
|
34
|
+
*
|
|
35
|
+
* Exit codes per `git check-ignore --quiet`:
|
|
36
|
+
* 0 — file IS ignored
|
|
37
|
+
* 1 — file is NOT ignored
|
|
38
|
+
* 128 — error (no git repo, etc.) — treat as not-ignored to avoid noisy
|
|
39
|
+
* false-positive warnings on broken installs
|
|
40
|
+
*/
|
|
41
|
+
const DEFAULT_CHECK_IGNORE = (repoRoot, relpath) => {
|
|
42
|
+
try {
|
|
43
|
+
const result = spawnSync(
|
|
44
|
+
'git',
|
|
45
|
+
['-C', repoRoot, 'check-ignore', '--quiet', relpath],
|
|
46
|
+
{ stdio: 'pipe' },
|
|
47
|
+
);
|
|
48
|
+
return result.status === 0;
|
|
49
|
+
} catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Variable substitution table for `kind: "interpolated"` templates.
|
|
56
|
+
*
|
|
57
|
+
* Each value is computed lazily from `opts` (the engine call site provides
|
|
58
|
+
* `repoRoot`). Adding a new variable here means it becomes referenceable as
|
|
59
|
+
* `{{NAME}}` inside any interpolated template.
|
|
60
|
+
*
|
|
61
|
+
* REPO_BASENAME is the leaf directory name of `repoRoot` after `path.resolve`
|
|
62
|
+
* — exactly what Codex Cloud uses as the project key (`/workspace/<basename>`).
|
|
63
|
+
*/
|
|
64
|
+
const SUBSTITUTIONS = {
|
|
65
|
+
REPO_BASENAME: (opts) => basename(resolve(opts.repoRoot)),
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Render `{{VAR}}` placeholders in `content` using the SUBSTITUTIONS table.
|
|
70
|
+
*
|
|
71
|
+
* Throws on any `{{...}}` reference whose key isn't in the table — fail loud
|
|
72
|
+
* over silently shipping `{{REPO_BASENAME}}` in a config file.
|
|
73
|
+
*
|
|
74
|
+
* @param {string} content
|
|
75
|
+
* @param {{repoRoot: string}} opts
|
|
76
|
+
* @returns {string} content with all known placeholders substituted
|
|
77
|
+
*/
|
|
78
|
+
function renderInterpolated(content, opts) {
|
|
79
|
+
return content.replace(/\{\{(\w+)\}\}/g, (_, name) => {
|
|
80
|
+
if (!Object.prototype.hasOwnProperty.call(SUBSTITUTIONS, name)) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
`init-repo: unknown variable {{${name}}} in interpolated template. ` +
|
|
83
|
+
`Known: ${Object.keys(SUBSTITUTIONS).join(', ')}.`,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
return SUBSTITUTIONS[name](opts);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Render the full expected file content for a `kind: "managed-block"` template.
|
|
92
|
+
*
|
|
93
|
+
* Reads the bundled block source (just the inner lines), strips any existing
|
|
94
|
+
* managed block bearing the same `marker` from the dest file (so re-runs
|
|
95
|
+
* cleanly replace stale content), and appends a fresh marker-wrapped block.
|
|
96
|
+
*
|
|
97
|
+
* Format on disk (matches existing AW convention from integrate.mjs and
|
|
98
|
+
* repoRootInstructions.mjs — colon-no-space, marker name on both ends):
|
|
99
|
+
* <pre-existing user content (managed block stripped)>
|
|
100
|
+
*
|
|
101
|
+
* # aw-managed:start <marker> (DO NOT EDIT — managed by `aw init-repo`)
|
|
102
|
+
* <block source content>
|
|
103
|
+
* # aw-managed:end <marker>
|
|
104
|
+
*
|
|
105
|
+
* The single blank line separator between user content and our block is only
|
|
106
|
+
* emitted when prior content exists. The file always ends with one trailing
|
|
107
|
+
* newline.
|
|
108
|
+
*
|
|
109
|
+
* @param {{src: string, marker: string}} template
|
|
110
|
+
* @param {{templatesDir: string, repoRoot: string}} opts
|
|
111
|
+
* @returns {string} full expected file content
|
|
112
|
+
*/
|
|
113
|
+
function renderManagedBlock(template, opts) {
|
|
114
|
+
const blockSrcPath = join(opts.templatesDir, template.src);
|
|
115
|
+
const blockBody = readFileSync(blockSrcPath, 'utf8').replace(/\n+$/, '');
|
|
116
|
+
|
|
117
|
+
const destPath = join(opts.repoRoot, template.dest);
|
|
118
|
+
const existing = existsSync(destPath) ? readFileSync(destPath, 'utf8') : '';
|
|
119
|
+
const preserved = stripManagedBlock(existing, template.marker).replace(/\n+$/, '');
|
|
120
|
+
|
|
121
|
+
const wrapped =
|
|
122
|
+
`# aw-managed:start ${template.marker} (DO NOT EDIT — managed by \`aw init-repo\`)\n` +
|
|
123
|
+
blockBody +
|
|
124
|
+
'\n' +
|
|
125
|
+
`# aw-managed:end ${template.marker}\n`;
|
|
126
|
+
|
|
127
|
+
return preserved.length > 0 ? `${preserved}\n\n${wrapped}` : wrapped;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Remove every `# aw-managed:start <marker> ... # aw-managed:end <marker>`
|
|
132
|
+
* block from `content`. Idempotent on content with no such block.
|
|
133
|
+
*
|
|
134
|
+
* Defensive against multiple stale blocks with the same marker (all collapse
|
|
135
|
+
* to none — fresh block is appended afterward by the caller).
|
|
136
|
+
*/
|
|
137
|
+
function stripManagedBlock(content, marker) {
|
|
138
|
+
const safeMarker = marker.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
139
|
+
const re = new RegExp(
|
|
140
|
+
`(?:^|\\n)# aw-managed:start ${safeMarker}(?:[^\\n]*)\\n` +
|
|
141
|
+
`[\\s\\S]*?` +
|
|
142
|
+
`# aw-managed:end ${safeMarker}[ \\t]*\\n?`,
|
|
143
|
+
'g',
|
|
144
|
+
);
|
|
145
|
+
return content.replace(re, '\n');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Default fs surface — node:fs. Tests inject mocks for failure paths. */
|
|
149
|
+
const DEFAULT_FS = {
|
|
150
|
+
existsSync,
|
|
151
|
+
readFileSync,
|
|
152
|
+
writeFileSync,
|
|
153
|
+
mkdirSync,
|
|
154
|
+
chmodSync,
|
|
155
|
+
statSync,
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
/** Default writer — process.stdout / process.stderr. */
|
|
159
|
+
const DEFAULT_WRITER = {
|
|
160
|
+
stdout: (s) => process.stdout.write(s),
|
|
161
|
+
stderr: (s) => process.stderr.write(s),
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
165
|
+
|
|
166
|
+
/** Default location of bundled templates inside the npm package. */
|
|
167
|
+
export const DEFAULT_TEMPLATES_DIR = join(__dirname, 'templates');
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Read & parse the templates manifest from the given directory.
|
|
171
|
+
*
|
|
172
|
+
* Throws a structured Error with a `code: 'TEMPLATES_NOT_FOUND'` marker
|
|
173
|
+
* when manifest.json is missing or unreadable — consumers (the
|
|
174
|
+
* orchestrator) translate this to user-facing exit-2 output.
|
|
175
|
+
*
|
|
176
|
+
* @param {string} templatesDir absolute path to the bundled templates dir
|
|
177
|
+
* @returns {{ version: 1, templates: Array<{kind:'literal',src:string,dest:string,mode:string}> }}
|
|
178
|
+
*/
|
|
179
|
+
export function loadManifest(templatesDir) {
|
|
180
|
+
const manifestPath = join(templatesDir, 'manifest.json');
|
|
181
|
+
let raw;
|
|
182
|
+
try {
|
|
183
|
+
raw = readFileSync(manifestPath, 'utf8');
|
|
184
|
+
} catch (err) {
|
|
185
|
+
const e = new Error(
|
|
186
|
+
`init-repo: bundled templates not found at ${manifestPath}. Reinstall @ghl-ai/aw.`,
|
|
187
|
+
);
|
|
188
|
+
e.code = 'TEMPLATES_NOT_FOUND';
|
|
189
|
+
e.cause = err;
|
|
190
|
+
throw e;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
let parsed;
|
|
194
|
+
try {
|
|
195
|
+
parsed = JSON.parse(raw);
|
|
196
|
+
} catch (err) {
|
|
197
|
+
const e = new Error(
|
|
198
|
+
`init-repo: bundled templates manifest.json is malformed at ${manifestPath}: ${err.message}`,
|
|
199
|
+
);
|
|
200
|
+
e.code = 'TEMPLATES_MALFORMED';
|
|
201
|
+
e.cause = err;
|
|
202
|
+
throw e;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return parsed;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Render the expected file content for a single manifest entry.
|
|
210
|
+
*
|
|
211
|
+
* Dispatch by `template.kind`:
|
|
212
|
+
* - 'literal' → readFileSync of the bundled src verbatim
|
|
213
|
+
* - 'interpolated' → readFileSync + {{VAR}} substitution via SUBSTITUTIONS
|
|
214
|
+
* - 'managed-block' → strip stale managed block from dest, append fresh
|
|
215
|
+
* marker-wrapped block built from src body. Returns the
|
|
216
|
+
* FULL rendered dest content (preserves user content
|
|
217
|
+
* outside the markers).
|
|
218
|
+
*
|
|
219
|
+
* Pure read — no FS writes. Tests can exercise this directly. The action
|
|
220
|
+
* object carries both expected and current content so applyActions and the
|
|
221
|
+
* diff renderer don't have to re-read.
|
|
222
|
+
*
|
|
223
|
+
* @param {{kind?: string, src: string, dest?: string, marker?: string}} template
|
|
224
|
+
* @param {{templatesDir: string, repoRoot?: string}} opts
|
|
225
|
+
* @returns {string} rendered template content (UTF-8)
|
|
226
|
+
*/
|
|
227
|
+
export function expectedContent(template, opts) {
|
|
228
|
+
const kind = template.kind ?? 'literal';
|
|
229
|
+
|
|
230
|
+
if (kind === 'literal') {
|
|
231
|
+
return readFileSync(join(opts.templatesDir, template.src), 'utf8');
|
|
232
|
+
}
|
|
233
|
+
if (kind === 'interpolated') {
|
|
234
|
+
const raw = readFileSync(join(opts.templatesDir, template.src), 'utf8');
|
|
235
|
+
return renderInterpolated(raw, opts);
|
|
236
|
+
}
|
|
237
|
+
if (kind === 'managed-block') {
|
|
238
|
+
return renderManagedBlock(template, opts);
|
|
239
|
+
}
|
|
240
|
+
throw new Error(
|
|
241
|
+
`init-repo: unknown template kind "${kind}" for ${template.src ?? '?'}.`,
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* sha256(content) hex — byte-exact, no normalization.
|
|
247
|
+
*
|
|
248
|
+
* Note: this is sensitive to line endings and BOM. On Windows with
|
|
249
|
+
* core.autocrlf=true a CRLF-rewritten clone will hash-mismatch and surface
|
|
250
|
+
* as `blocked-edited` even on a clean checkout. spec.md::"Cross-platform
|
|
251
|
+
* line endings" recommends a repo-local .gitattributes with `* eol=lf`.
|
|
252
|
+
*/
|
|
253
|
+
function sha256(content) {
|
|
254
|
+
return createHash('sha256').update(content, 'utf8').digest('hex');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Decide per-file action for every template — pure, no FS writes.
|
|
259
|
+
*
|
|
260
|
+
* Decision matrix:
|
|
261
|
+
* file missing → 'create'
|
|
262
|
+
* file exists, hash matches → 'skip-equal'
|
|
263
|
+
* file exists, hash differs:
|
|
264
|
+
* opts.diff → 'diff' (read-only inspection)
|
|
265
|
+
* opts.force → 'overwrite' (clobber edits)
|
|
266
|
+
* else → 'blocked-edited' (exit 1, no write)
|
|
267
|
+
*
|
|
268
|
+
* @param {{repoRoot: string, templatesDir: string, force?: boolean, diff?: boolean}} opts
|
|
269
|
+
* @param {{templates: Array}} manifest
|
|
270
|
+
* @returns {Array<{template: object, dest: string, action: string, expectedContent: string, currentContent: string|null}>}
|
|
271
|
+
*/
|
|
272
|
+
export function planActions(opts, manifest) {
|
|
273
|
+
return manifest.templates.map((template) => {
|
|
274
|
+
const dest = join(opts.repoRoot, template.dest);
|
|
275
|
+
const expected = expectedContent(template, opts);
|
|
276
|
+
|
|
277
|
+
if (!existsSync(dest)) {
|
|
278
|
+
return {
|
|
279
|
+
template,
|
|
280
|
+
dest,
|
|
281
|
+
action: 'create',
|
|
282
|
+
expectedContent: expected,
|
|
283
|
+
currentContent: null,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const current = readFileSync(dest, 'utf8');
|
|
288
|
+
const isEqual = sha256(current) === sha256(expected);
|
|
289
|
+
|
|
290
|
+
if (isEqual) {
|
|
291
|
+
return {
|
|
292
|
+
template,
|
|
293
|
+
dest,
|
|
294
|
+
action: 'skip-equal',
|
|
295
|
+
expectedContent: expected,
|
|
296
|
+
currentContent: current,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
let action;
|
|
301
|
+
if (template.kind === 'managed-block') {
|
|
302
|
+
// We own the marker-bracketed area; user edits OUTSIDE the markers are
|
|
303
|
+
// preserved by renderManagedBlock. Mismatch always means "our block is
|
|
304
|
+
// stale or missing" → safe to overwrite.
|
|
305
|
+
action = 'overwrite';
|
|
306
|
+
} else if (opts.diff) {
|
|
307
|
+
action = 'diff';
|
|
308
|
+
} else if (opts.force) {
|
|
309
|
+
action = 'overwrite';
|
|
310
|
+
} else {
|
|
311
|
+
action = 'blocked-edited';
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return {
|
|
315
|
+
template,
|
|
316
|
+
dest,
|
|
317
|
+
action,
|
|
318
|
+
expectedContent: expected,
|
|
319
|
+
currentContent: current,
|
|
320
|
+
};
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Apply planned actions to the filesystem.
|
|
326
|
+
*
|
|
327
|
+
* - 'create' / 'overwrite' → mkdirSync (recursive) + writeFileSync + chmodSync
|
|
328
|
+
* - 'skip-equal' / 'blocked-edited' / 'diff' → no-op (no FS write)
|
|
329
|
+
*
|
|
330
|
+
* No-op entirely when opts.dryRun is true. Non-atomic — partial-write
|
|
331
|
+
* failures self-heal on the next run (see spec.md "Atomicity note").
|
|
332
|
+
*
|
|
333
|
+
* chmod is best-effort: wrapped in try/catch for EPERM/ENOTSUP/ENOSYS.
|
|
334
|
+
* On chmod failure we emit a warn line via opts.writer.stderr and
|
|
335
|
+
* continue with exit 0 — the file write is the primary outcome and the
|
|
336
|
+
* x-bit is recoverable manually with `chmod +x <path>`.
|
|
337
|
+
*
|
|
338
|
+
* @param {{repoRoot:string, templatesDir:string, dryRun?:boolean, fs?:object, writer?:object}} opts
|
|
339
|
+
* @param {Array} actions output of planActions
|
|
340
|
+
*/
|
|
341
|
+
export function applyActions(opts, actions) {
|
|
342
|
+
if (opts.dryRun) return;
|
|
343
|
+
|
|
344
|
+
const fs = opts.fs ?? DEFAULT_FS;
|
|
345
|
+
const writer = opts.writer ?? DEFAULT_WRITER;
|
|
346
|
+
|
|
347
|
+
for (const action of actions) {
|
|
348
|
+
if (action.action !== 'create' && action.action !== 'overwrite') continue;
|
|
349
|
+
|
|
350
|
+
fs.mkdirSync(dirname(action.dest), { recursive: true });
|
|
351
|
+
fs.writeFileSync(action.dest, action.expectedContent);
|
|
352
|
+
|
|
353
|
+
if (action.template.mode) {
|
|
354
|
+
try {
|
|
355
|
+
fs.chmodSync(action.dest, parseInt(action.template.mode, 8));
|
|
356
|
+
} catch (err) {
|
|
357
|
+
writer.stderr(
|
|
358
|
+
`init-repo: WARN: failed to chmod ${action.dest}: ${err.code ?? err.message}. ` +
|
|
359
|
+
`File written but executable bit may be missing — set manually with chmod +x ${action.dest}.\n`,
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Run `git check-ignore` against every scaffolded file and return the list
|
|
368
|
+
* of paths still ignored after applyActions wrote the managed `.gitignore`
|
|
369
|
+
* block. Surfaces pathological gitignore patterns we can't override (most
|
|
370
|
+
* notably dir-only rules like `.codex/` — git refuses to look inside an
|
|
371
|
+
* excluded directory regardless of subsequent `!` rules).
|
|
372
|
+
*
|
|
373
|
+
* Skips:
|
|
374
|
+
* - managed-block kind (`.gitignore` itself isn't normally tracked)
|
|
375
|
+
* - blocked-edited / diff actions (file content is user's, not ours)
|
|
376
|
+
*
|
|
377
|
+
* @param {{repoRoot: string, checkIgnore?: (root: string, p: string) => boolean}} opts
|
|
378
|
+
* @param {Array} actions
|
|
379
|
+
* @returns {string[]} list of dest paths still ignored
|
|
380
|
+
*/
|
|
381
|
+
function verifyIgnoreStatus(opts, actions) {
|
|
382
|
+
const checkIgnore = opts.checkIgnore ?? DEFAULT_CHECK_IGNORE;
|
|
383
|
+
const stillIgnored = [];
|
|
384
|
+
|
|
385
|
+
for (const action of actions) {
|
|
386
|
+
if (action.template.kind === 'managed-block') continue;
|
|
387
|
+
if (action.action === 'blocked-edited' || action.action === 'diff') continue;
|
|
388
|
+
|
|
389
|
+
if (checkIgnore(opts.repoRoot, action.template.dest)) {
|
|
390
|
+
stillIgnored.push(action.template.dest);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return stillIgnored;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Build a stderr WARNING block for paths still ignored after applyActions.
|
|
398
|
+
*
|
|
399
|
+
* Most common cause: dir-only patterns (`.codex/`, `.claude/`) — git docs
|
|
400
|
+
* explicitly forbid re-include of files under an excluded directory. The
|
|
401
|
+
* fix is user-side, so we surface the exact paths and the change they need.
|
|
402
|
+
*/
|
|
403
|
+
function buildIgnoreWarning(paths) {
|
|
404
|
+
return [
|
|
405
|
+
'',
|
|
406
|
+
`init-repo: WARN: ${paths.length} scaffolded file${paths.length === 1 ? '' : 's'} still git-ignored:`,
|
|
407
|
+
...paths.map((p) => ` - ${p}`),
|
|
408
|
+
'',
|
|
409
|
+
'Your .gitignore has rules that block our managed re-include — most likely',
|
|
410
|
+
'dir-only patterns (e.g. `.codex/`, `.claude/`, `.cursor/`). Per gitignore',
|
|
411
|
+
'docs, files cannot be re-included once a parent directory is excluded.',
|
|
412
|
+
'',
|
|
413
|
+
'Fix: change those rules to use a trailing `*` so they ignore CONTENTS rather',
|
|
414
|
+
'than the directory itself, then re-run `aw init-repo`. Examples:',
|
|
415
|
+
' .codex/ → .codex/*',
|
|
416
|
+
' .claude/ → .claude/*',
|
|
417
|
+
' .cursor/ → .cursor/*',
|
|
418
|
+
'',
|
|
419
|
+
].join('\n');
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Format a single action line for the summary block.
|
|
424
|
+
*
|
|
425
|
+
* The 18-char tag width keeps `[blocked-edited]` aligned with shorter tags.
|
|
426
|
+
*/
|
|
427
|
+
function formatActionLine(action) {
|
|
428
|
+
const tag = `[${action.action}]`.padEnd(18, ' ');
|
|
429
|
+
return ` ${tag} ${action.template.dest}`;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Build the multi-line summary printed to stdout.
|
|
434
|
+
*
|
|
435
|
+
* Distinguishes dry-run vs apply, reports counts per action kind, and
|
|
436
|
+
* surfaces blocked-edited remediation hints inline so users don't have
|
|
437
|
+
* to dig through docs.
|
|
438
|
+
*/
|
|
439
|
+
function buildSummary({ repoRoot, dryRun, actions, blockedCount, exitCode }) {
|
|
440
|
+
const head = dryRun
|
|
441
|
+
? `dry-run — would scaffold ${actions.length} files into ${repoRoot}`
|
|
442
|
+
: `Scaffolding ${actions.length} files into ${repoRoot}`;
|
|
443
|
+
|
|
444
|
+
const lines = [`init-repo: ${head}`, ...actions.map(formatActionLine), ''];
|
|
445
|
+
|
|
446
|
+
if (blockedCount > 0 && exitCode === 1) {
|
|
447
|
+
lines.push(
|
|
448
|
+
`${blockedCount} file${blockedCount === 1 ? '' : 's'} blocked ` +
|
|
449
|
+
`(locally edited; pass --force to overwrite or --diff to inspect).`,
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (dryRun) {
|
|
454
|
+
lines.push(`Run without --dry-run to write. exit ${exitCode}.`);
|
|
455
|
+
} else if (exitCode === 0) {
|
|
456
|
+
lines.push(`Done. exit 0.`);
|
|
457
|
+
} else {
|
|
458
|
+
lines.push(`exit ${exitCode}.`);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return lines.join('\n');
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Public orchestrator. Returns a {exitCode, summary, actions} record;
|
|
466
|
+
* the command-handler translates exitCode !== 0 to a CancelError.
|
|
467
|
+
*
|
|
468
|
+
* Validates inputs (git repo guard, --namespace rejection), composes
|
|
469
|
+
* loadManifest → planActions → applyActions, prints the summary via
|
|
470
|
+
* opts.writer, and never throws on user errors — every failure is
|
|
471
|
+
* encoded in exitCode + summary.
|
|
472
|
+
*
|
|
473
|
+
* @param {{
|
|
474
|
+
* repoRoot: string,
|
|
475
|
+
* templatesDir?: string,
|
|
476
|
+
* namespace?: string,
|
|
477
|
+
* force?: boolean,
|
|
478
|
+
* diff?: boolean,
|
|
479
|
+
* dryRun?: boolean,
|
|
480
|
+
* noGitignore?: boolean,
|
|
481
|
+
* checkIgnore?: (root: string, p: string) => boolean,
|
|
482
|
+
* fs?: object,
|
|
483
|
+
* writer?: object,
|
|
484
|
+
* }} opts
|
|
485
|
+
* @returns {{ exitCode: number, summary: string, actions: Array }}
|
|
486
|
+
*/
|
|
487
|
+
export function initRepo(opts) {
|
|
488
|
+
const writer = opts.writer ?? DEFAULT_WRITER;
|
|
489
|
+
const fs = opts.fs ?? DEFAULT_FS;
|
|
490
|
+
const templatesDir = opts.templatesDir ?? DEFAULT_TEMPLATES_DIR;
|
|
491
|
+
const repoRoot = opts.repoRoot;
|
|
492
|
+
|
|
493
|
+
// Guard: --namespace not supported in v1.
|
|
494
|
+
if (opts.namespace) {
|
|
495
|
+
const summary =
|
|
496
|
+
`init-repo: --namespace is not supported in v1. ` +
|
|
497
|
+
`init-repo only scaffolds the 4 cloud-bootstrap files. ` +
|
|
498
|
+
`Use 'aw init --namespace <team>' for namespace pulls.`;
|
|
499
|
+
writer.stderr(summary + '\n');
|
|
500
|
+
return { exitCode: 2, summary, actions: [] };
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Guard: repoRoot must be a git repo (.git can be a directory or a file
|
|
504
|
+
// — git worktrees use a file at .git that points to the worktree dir).
|
|
505
|
+
if (!fs.existsSync(join(repoRoot, '.git'))) {
|
|
506
|
+
const summary =
|
|
507
|
+
`init-repo: ${repoRoot} is not a git repository (no .git found). ` +
|
|
508
|
+
`Run 'git init' first or pass a path to an existing repo.`;
|
|
509
|
+
writer.stderr(summary + '\n');
|
|
510
|
+
return { exitCode: 2, summary, actions: [] };
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Load + plan + apply.
|
|
514
|
+
let manifest;
|
|
515
|
+
try {
|
|
516
|
+
manifest = loadManifest(templatesDir);
|
|
517
|
+
} catch (err) {
|
|
518
|
+
writer.stderr(err.message + '\n');
|
|
519
|
+
return { exitCode: 2, summary: err.message, actions: [] };
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// --no-gitignore opt-out: drop every managed-block template before planning.
|
|
523
|
+
// Users who manage their .gitignore by hand can pass this flag to keep
|
|
524
|
+
// init-repo from injecting the cloud-bootstrap re-include block.
|
|
525
|
+
const filteredManifest = opts.noGitignore
|
|
526
|
+
? { ...manifest, templates: manifest.templates.filter(t => t.kind !== 'managed-block') }
|
|
527
|
+
: manifest;
|
|
528
|
+
|
|
529
|
+
const actions = planActions(
|
|
530
|
+
{ repoRoot, templatesDir, force: opts.force, diff: opts.diff, fs },
|
|
531
|
+
filteredManifest,
|
|
532
|
+
);
|
|
533
|
+
|
|
534
|
+
applyActions(
|
|
535
|
+
{ repoRoot, templatesDir, dryRun: opts.dryRun, fs, writer },
|
|
536
|
+
actions,
|
|
537
|
+
);
|
|
538
|
+
|
|
539
|
+
const blockedCount = actions.filter(a => a.action === 'blocked-edited').length;
|
|
540
|
+
const exitCode = blockedCount > 0 ? 1 : 0;
|
|
541
|
+
|
|
542
|
+
const summary = buildSummary({
|
|
543
|
+
repoRoot,
|
|
544
|
+
dryRun: !!opts.dryRun,
|
|
545
|
+
actions,
|
|
546
|
+
blockedCount,
|
|
547
|
+
exitCode,
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
writer.stdout(summary + '\n');
|
|
551
|
+
|
|
552
|
+
// Post-apply: verify that scaffolded paths are actually trackable by git.
|
|
553
|
+
// Emitted AFTER the summary so the action list always renders first
|
|
554
|
+
// regardless of stdout/stderr buffering. Skip on dryRun — nothing was
|
|
555
|
+
// written, so check-ignore would probe stale state and emit noise.
|
|
556
|
+
if (!opts.dryRun) {
|
|
557
|
+
const stillIgnored = verifyIgnoreStatus(
|
|
558
|
+
{ repoRoot, checkIgnore: opts.checkIgnore },
|
|
559
|
+
actions,
|
|
560
|
+
);
|
|
561
|
+
if (stillIgnored.length > 0) {
|
|
562
|
+
writer.stderr(buildIgnoreWarning(stillIgnored) + '\n');
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
return { exitCode, summary, actions };
|
|
567
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# .claude/scripts/claude-web-bootstrap.sh — Claude Code Web entry shim.
|
|
3
|
+
#
|
|
4
|
+
# Claude Code Web's external Startup Command is configured to run this file.
|
|
5
|
+
# Path is preserved so existing CCW UI configs keep working; logic is
|
|
6
|
+
# delegated to scripts/aw-c4-bootstrap.sh.
|
|
7
|
+
set -Eeuo pipefail
|
|
8
|
+
exec bash "$(dirname "$0")/../../scripts/aw-c4-bootstrap.sh" --harness claude-web "$@"
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# .codex/scripts/codex-web-bootstrap.sh — Codex Web entry shim.
|
|
3
|
+
#
|
|
4
|
+
# Codex Web's external setup script is configured to run this file. Path is
|
|
5
|
+
# preserved so existing Codex UI configs keep working; logic is delegated to
|
|
6
|
+
# scripts/aw-c4-bootstrap.sh.
|
|
7
|
+
set -Eeuo pipefail
|
|
8
|
+
exec bash "$(dirname "$0")/../../scripts/aw-c4-bootstrap.sh" --harness codex-web "$@"
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Cloud bootstrap files — re-include for tracking even when parent dirs are
|
|
2
|
+
# ignored (e.g. .codex/*, .claude/*, .cursor/*). Order matters: a file under
|
|
3
|
+
# an excluded directory cannot be un-ignored without first un-ignoring the
|
|
4
|
+
# directory. We only un-ignore parent paths and whitelist AW-managed files —
|
|
5
|
+
# we do NOT re-ignore other content inside these dirs, so users' own scripts
|
|
6
|
+
# stay trackable. Hook-logs and other ephemeral state are managed elsewhere
|
|
7
|
+
# (see `aw init`'s repoLocalIgnore manager).
|
|
8
|
+
!scripts/aw-c4-bootstrap.sh
|
|
9
|
+
!.codex/config.toml
|
|
10
|
+
!.cursor/environment.json
|
|
11
|
+
!.codex/scripts/
|
|
12
|
+
!.codex/scripts/codex-web-bootstrap.sh
|
|
13
|
+
!.claude/scripts/
|
|
14
|
+
!.claude/scripts/claude-web-bootstrap.sh
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"templates": [
|
|
4
|
+
{
|
|
5
|
+
"kind": "literal",
|
|
6
|
+
"src": "scripts/aw-c4-bootstrap.sh",
|
|
7
|
+
"dest": "scripts/aw-c4-bootstrap.sh",
|
|
8
|
+
"mode": "0755"
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"kind": "literal",
|
|
12
|
+
"src": "codex/scripts/codex-web-bootstrap.sh",
|
|
13
|
+
"dest": ".codex/scripts/codex-web-bootstrap.sh",
|
|
14
|
+
"mode": "0755"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"kind": "literal",
|
|
18
|
+
"src": "claude/scripts/claude-web-bootstrap.sh",
|
|
19
|
+
"dest": ".claude/scripts/claude-web-bootstrap.sh",
|
|
20
|
+
"mode": "0755"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"kind": "literal",
|
|
24
|
+
"src": "cursor/environment.json",
|
|
25
|
+
"dest": ".cursor/environment.json",
|
|
26
|
+
"mode": "0644"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"kind": "interpolated",
|
|
30
|
+
"src": "codex/config.toml",
|
|
31
|
+
"dest": ".codex/config.toml",
|
|
32
|
+
"mode": "0644"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"kind": "managed-block",
|
|
36
|
+
"src": "gitignore-block.txt",
|
|
37
|
+
"dest": ".gitignore",
|
|
38
|
+
"marker": "cloud-bootstrap"
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# scripts/aw-c4-bootstrap.sh — single-entry cloud bootstrap for AW.
|
|
3
|
+
#
|
|
4
|
+
# Installs @ghl-ai/aw and runs `aw c4`, which handles ALL harness-specific
|
|
5
|
+
# wiring (slim router card, hooks, MCP server, repo-root context, ECC bridge,
|
|
6
|
+
# Codex prompt-injector, GitHub auth). Forward any extra args to `aw c4` so
|
|
7
|
+
# `--dry-run` and `--diagnose` flow through untouched.
|
|
8
|
+
#
|
|
9
|
+
# Invoked from:
|
|
10
|
+
# - .cursor/environment.json::install (passes --harness cursor-cloud)
|
|
11
|
+
# - .claude/scripts/claude-web-bootstrap.sh (passes --harness claude-web)
|
|
12
|
+
# - .codex/scripts/codex-web-bootstrap.sh (passes --harness codex-web)
|
|
13
|
+
#
|
|
14
|
+
# Override knobs (env):
|
|
15
|
+
# AW_PACKAGE npm spec to install. Defaults to @ghl-ai/aw@latest. Override
|
|
16
|
+
# with @ghl-ai/aw@beta to opt into pre-release builds, or pin to
|
|
17
|
+
# @ghl-ai/aw@0.1.x for reproducible CI runs.
|
|
18
|
+
set -Eeuo pipefail
|
|
19
|
+
|
|
20
|
+
: "${GITHUB_PAT:?ERROR: Set GITHUB_PAT in your harness secrets UI before running aw c4}"
|
|
21
|
+
|
|
22
|
+
# Ensure npm is on PATH. Cursor Cloud's install shell is non-interactive — nvm
|
|
23
|
+
# is not auto-sourced, and Node may not be pre-installed at all. Walk common
|
|
24
|
+
# locations first, then fall back to a NodeSource apt install (Ubuntu base).
|
|
25
|
+
#
|
|
26
|
+
# Hardening (vs the original):
|
|
27
|
+
# - Search nvm in multiple home dirs (current user, root, ubuntu).
|
|
28
|
+
# - Probe pre-installed Node binaries off the default PATH before falling
|
|
29
|
+
# through to apt — much faster when the agent snapshot ships Node in
|
|
30
|
+
# /usr/local/ or /opt/.
|
|
31
|
+
# - DEBIAN_FRONTEND=noninteractive prevents debconf prompts from hanging
|
|
32
|
+
# the apt configure step on a non-TTY shell.
|
|
33
|
+
# - Drop the >/dev/null on apt + NodeSource setup so progress is visible.
|
|
34
|
+
# The previous "silent + interactive" combination produced the canonical
|
|
35
|
+
# "looks frozen" symptom in pilot use after Cursor Cloud stopped shipping
|
|
36
|
+
# Node in their base image.
|
|
37
|
+
# - timeout 180 is the hard ceiling — fail fast with a clear error instead
|
|
38
|
+
# of hanging the whole agent boot.
|
|
39
|
+
ensure_npm() {
|
|
40
|
+
command -v npm >/dev/null 2>&1 && return 0
|
|
41
|
+
|
|
42
|
+
for nvm_dir in "${NVM_DIR:-}" "$HOME/.nvm" /root/.nvm /home/ubuntu/.nvm; do
|
|
43
|
+
[ -z "$nvm_dir" ] && continue
|
|
44
|
+
if [ -s "$nvm_dir/nvm.sh" ]; then
|
|
45
|
+
export NVM_DIR="$nvm_dir"
|
|
46
|
+
# shellcheck source=/dev/null
|
|
47
|
+
. "$NVM_DIR/nvm.sh"
|
|
48
|
+
nvm use --lts >/dev/null 2>&1 || nvm install --lts >/dev/null 2>&1 || true
|
|
49
|
+
command -v npm >/dev/null 2>&1 && return 0
|
|
50
|
+
fi
|
|
51
|
+
done
|
|
52
|
+
|
|
53
|
+
for node_bin in /usr/local/bin/node /opt/node/bin/node; do
|
|
54
|
+
if [ -x "$node_bin" ]; then
|
|
55
|
+
export PATH="$(dirname "$node_bin"):$PATH"
|
|
56
|
+
command -v npm >/dev/null 2>&1 && return 0
|
|
57
|
+
fi
|
|
58
|
+
done
|
|
59
|
+
|
|
60
|
+
if command -v sudo >/dev/null 2>&1 && command -v apt-get >/dev/null 2>&1; then
|
|
61
|
+
echo "[aw-c4-bootstrap] npm not found; installing Node 20 via NodeSource (1-2 min over corporate proxy)"
|
|
62
|
+
export DEBIAN_FRONTEND=noninteractive
|
|
63
|
+
if ! timeout 180 bash -c 'curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -'; then
|
|
64
|
+
echo "[aw-c4-bootstrap] FATAL: NodeSource setup script timed out or failed after 180s" >&2
|
|
65
|
+
echo "[aw-c4-bootstrap] Preinstall Node 20+ in the agent snapshot, or check proxy reachability." >&2
|
|
66
|
+
exit 1
|
|
67
|
+
fi
|
|
68
|
+
if ! timeout 180 sudo -E apt-get install -y nodejs; then
|
|
69
|
+
echo "[aw-c4-bootstrap] FATAL: apt-get install nodejs timed out or failed after 180s" >&2
|
|
70
|
+
exit 1
|
|
71
|
+
fi
|
|
72
|
+
fi
|
|
73
|
+
|
|
74
|
+
command -v npm >/dev/null 2>&1 || {
|
|
75
|
+
echo "[aw-c4-bootstrap] FATAL: npm not available; preinstall Node 20+ in the harness snapshot" >&2
|
|
76
|
+
exit 1
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
ensure_npm
|
|
81
|
+
|
|
82
|
+
# Codex Cloud (and other corporate-MITM cloud agents) intercept HTTPS via an
|
|
83
|
+
# Envoy-style proxy with a private CA at /usr/local/share/ca-certificates/.
|
|
84
|
+
# curl/git/npm read the system CA bundle and Just Work, but Node uses its
|
|
85
|
+
# own bundle. Bridging requires NODE_EXTRA_CA_CERTS pointed at the MITM CA;
|
|
86
|
+
# without it, `aw c4`'s preflight `fetch()` calls fail with TLS errors even
|
|
87
|
+
# when curl + git ls-remote succeed against the same hosts.
|
|
88
|
+
ENVOY_CA="/usr/local/share/ca-certificates/envoy-mitmproxy-ca-cert.crt"
|
|
89
|
+
if [ -f "$ENVOY_CA" ] && [ -z "${NODE_EXTRA_CA_CERTS:-}" ]; then
|
|
90
|
+
export NODE_EXTRA_CA_CERTS="$ENVOY_CA"
|
|
91
|
+
echo "[aw-c4-bootstrap] enabled NODE_EXTRA_CA_CERTS=$ENVOY_CA"
|
|
92
|
+
fi
|
|
93
|
+
|
|
94
|
+
AW_PACKAGE="${AW_PACKAGE:-@ghl-ai/aw@latest}"
|
|
95
|
+
|
|
96
|
+
echo "[aw-c4-bootstrap] installing ${AW_PACKAGE}"
|
|
97
|
+
npm install -g "${AW_PACKAGE}"
|
|
98
|
+
|
|
99
|
+
echo "[aw-c4-bootstrap] running: aw c4 $*"
|
|
100
|
+
exec aw c4 "$@"
|
package/cli.mjs
CHANGED
|
@@ -28,6 +28,7 @@ const COMMANDS = {
|
|
|
28
28
|
telemetry: () => import('./commands/telemetry.mjs').then(m => m.telemetryCommand),
|
|
29
29
|
'slack-sim': () => import('./commands/slack-sim.mjs').then(m => m.slackSimCommand),
|
|
30
30
|
c4: () => import('./commands/c4.mjs').then(m => m.c4Command),
|
|
31
|
+
'init-repo': () => import('./commands/init-repo.mjs').then(m => m.initRepoCommand),
|
|
31
32
|
};
|
|
32
33
|
|
|
33
34
|
function parseArgs(argv) {
|
|
@@ -85,6 +86,7 @@ function printHelp() {
|
|
|
85
86
|
cmd('aw init --namespace <team/sub-team>', 'Add a team namespace (optional)'),
|
|
86
87
|
` ${chalk.dim('Teams: platform, revex, mobile, commerce, leadgen, crm, marketplace, ai')}`,
|
|
87
88
|
` ${chalk.dim('Example: aw init --namespace revex/courses')}`,
|
|
89
|
+
cmd('aw init-repo', 'Scaffold cloud-bootstrap files (idempotent, --dry-run/--force/--diff)'),
|
|
88
90
|
|
|
89
91
|
sec('Download'),
|
|
90
92
|
cmd('aw pull', 'Re-pull all synced paths (like git pull)'),
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* commands/init-repo.mjs — thin command-handler for `aw init-repo`.
|
|
3
|
+
*
|
|
4
|
+
* Translates the cli.mjs-parsed args object into engine options, calls
|
|
5
|
+
* c4/initRepo.mjs::initRepo(), and converts non-zero exit codes into
|
|
6
|
+
* CancelError throws so cli.mjs::run() preserves the telemetry-end and
|
|
7
|
+
* update-notify lifecycle (see G36 in spec.md::"Failure modes").
|
|
8
|
+
*
|
|
9
|
+
* Contract: spec.md::"libs/aw/commands/init-repo.mjs (command handler)"
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { initRepo as defaultInitRepo } from '../c4/initRepo.mjs';
|
|
13
|
+
import { CancelError } from '../fmt.mjs';
|
|
14
|
+
|
|
15
|
+
const HELP = `
|
|
16
|
+
aw init-repo — scaffold cloud-bootstrap files into the current repo
|
|
17
|
+
|
|
18
|
+
Usage:
|
|
19
|
+
aw init-repo Scaffold 6 files (idempotent: skip-equal on byte match)
|
|
20
|
+
aw init-repo --dry-run Preview actions without writing
|
|
21
|
+
aw init-repo --diff Show diffs for locally-edited files (no write)
|
|
22
|
+
aw init-repo --force Overwrite locally-edited files
|
|
23
|
+
aw init-repo --no-gitignore Skip the .gitignore managed block (manage it by hand)
|
|
24
|
+
|
|
25
|
+
Files written:
|
|
26
|
+
scripts/aw-c4-bootstrap.sh (unified bootstrap, executable)
|
|
27
|
+
.codex/scripts/codex-web-bootstrap.sh (executable)
|
|
28
|
+
.codex/config.toml (codex hooks + per-repo trust)
|
|
29
|
+
.claude/scripts/claude-web-bootstrap.sh (executable)
|
|
30
|
+
.cursor/environment.json (cursor cloud config)
|
|
31
|
+
.gitignore (managed block — re-include of the 5 above)
|
|
32
|
+
|
|
33
|
+
Exit codes:
|
|
34
|
+
0 All files created or already in sync
|
|
35
|
+
1 At least one file was locally edited (no write — pass --force or --diff)
|
|
36
|
+
2 Bad input (not a git repo, --namespace passed, missing templates)
|
|
37
|
+
|
|
38
|
+
Notes:
|
|
39
|
+
- Idempotent. SHA-256 byte-compare per file. Re-running on a clean repo is a no-op.
|
|
40
|
+
- --namespace is NOT supported (use 'aw init --namespace <team>' for namespace pulls).
|
|
41
|
+
- .cursor/environment.json is a literal template; manually-added terminals or
|
|
42
|
+
run-on-build entries will be overwritten by --force. Use --diff first.
|
|
43
|
+
- .codex/config.toml has {{REPO_BASENAME}} substituted at scaffold time so the
|
|
44
|
+
[projects."/workspace/<repo>"] trust block matches Codex Cloud's checkout path.
|
|
45
|
+
- .gitignore: a marker-bracketed block re-includes the 5 cloud-bootstrap files
|
|
46
|
+
even when parent dirs are ignored. User content outside the markers is preserved.
|
|
47
|
+
A WARN with paste-ready fix is printed if pathological dir-only rules
|
|
48
|
+
(e.g. \`.codex/\`, \`.claude/\`) prevent re-include — change those to
|
|
49
|
+
\`.codex/*\` / \`.claude/*\` and re-run.
|
|
50
|
+
`.trim();
|
|
51
|
+
|
|
52
|
+
function printHelp() {
|
|
53
|
+
console.log(HELP);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Command-handler entry. Resolves on success, throws CancelError on
|
|
58
|
+
* non-zero engine exit. Never calls process.exit — that is cli.mjs's job.
|
|
59
|
+
*
|
|
60
|
+
* @param {object} args parsed flags from cli.mjs::parseArgs
|
|
61
|
+
* @param {{ initRepo?: Function, cwd?: () => string }} [deps] testability injection
|
|
62
|
+
*/
|
|
63
|
+
export async function initRepoCommand(args, deps = {}) {
|
|
64
|
+
const _initRepo = deps.initRepo ?? defaultInitRepo;
|
|
65
|
+
const _cwd = deps.cwd ?? (() => process.cwd());
|
|
66
|
+
|
|
67
|
+
if (args['--help']) {
|
|
68
|
+
printHelp();
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// v1: --namespace is not part of init-repo. Reject early so users don't
|
|
73
|
+
// assume init-repo will fan out a registry pull on top of scaffolding.
|
|
74
|
+
if (args['--namespace']) {
|
|
75
|
+
throw new CancelError(
|
|
76
|
+
`aw init-repo: --namespace is not supported. ` +
|
|
77
|
+
`init-repo only scaffolds the 4 cloud-bootstrap files. ` +
|
|
78
|
+
`Use 'aw init --namespace <team>' for namespace pulls.`,
|
|
79
|
+
{ exitCode: 2 },
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const result = _initRepo({
|
|
84
|
+
repoRoot: _cwd(),
|
|
85
|
+
dryRun: !!args['--dry-run'],
|
|
86
|
+
force: !!args['--force'],
|
|
87
|
+
diff: !!args['--diff'],
|
|
88
|
+
noGitignore: !!args['--no-gitignore'],
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
if (result.exitCode !== 0) {
|
|
92
|
+
throw new CancelError(result.summary, { exitCode: result.exitCode });
|
|
93
|
+
}
|
|
94
|
+
}
|