godpowers 2.0.0 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +141 -0
  3. package/README.md +45 -5
  4. package/RELEASE.md +30 -48
  5. package/SKILL.md +9 -1
  6. package/agents/god-design-reviewer.md +6 -6
  7. package/agents/god-designer.md +1 -1
  8. package/agents/god-executor.md +23 -0
  9. package/agents/god-quality-reviewer.md +12 -1
  10. package/agents/god-spec-reviewer.md +10 -0
  11. package/bin/install.js +119 -655
  12. package/extensions/launch-pack/README.md +1 -1
  13. package/lib/README.md +16 -0
  14. package/lib/agent-browser-driver.js +13 -13
  15. package/lib/agent-cache.js +8 -1
  16. package/lib/agent-refs.js +161 -0
  17. package/lib/budget.js +25 -11
  18. package/lib/context-writer.js +17 -6
  19. package/lib/events.js +11 -4
  20. package/lib/extension-authoring.js +27 -0
  21. package/lib/feature-awareness.js +18 -0
  22. package/lib/fs-async.js +28 -0
  23. package/lib/installer-args.js +99 -0
  24. package/lib/installer-core.js +345 -0
  25. package/lib/installer-files.js +80 -0
  26. package/lib/installer-runtimes.js +112 -0
  27. package/lib/intent.js +111 -16
  28. package/lib/release-surface-sync.js +8 -1
  29. package/lib/repo-surface-sync.js +9 -2
  30. package/lib/review-required.js +2 -1
  31. package/lib/router.js +23 -3
  32. package/lib/skill-surface.js +42 -0
  33. package/lib/state-lock.js +10 -0
  34. package/lib/state.js +101 -8
  35. package/lib/workflow-runner.js +42 -5
  36. package/package.json +4 -3
  37. package/references/HAVE-NOTS.md +4 -3
  38. package/references/orchestration/GOD-MODE-RUNBOOK.md +273 -0
  39. package/routing/god-arch.yaml +1 -1
  40. package/routing/god-build.yaml +1 -1
  41. package/skills/god-add-backlog.md +1 -1
  42. package/skills/god-agent-audit.md +2 -2
  43. package/skills/god-build.md +5 -3
  44. package/skills/god-context-scan.md +2 -3
  45. package/skills/god-design.md +2 -2
  46. package/skills/god-doctor.md +2 -2
  47. package/skills/god-help.md +4 -3
  48. package/skills/god-mode.md +10 -266
  49. package/skills/god-org-context.md +1 -1
  50. package/skills/god-repair.md +3 -3
  51. package/skills/god-review.md +9 -0
  52. package/skills/god-stories.md +1 -1
  53. package/skills/god-version.md +2 -2
@@ -11,7 +11,7 @@ Channel-specific launch strategists for Godpowers.
11
11
  | `/god-indie-hackers` | god-indie-hackers-strategist | Numbers-first, mistakes, real questions |
12
12
  | `/god-oss-release` | god-oss-release-strategist | README, SemVer, examples that run |
13
13
 
14
- Plus 4 workflows and 22 channel-specific have-nots.
14
+ Plus 4 workflows and 23 channel-specific have-nots.
15
15
 
16
16
  ## When to use
17
17
 
package/lib/README.md CHANGED
@@ -22,6 +22,7 @@ package-level integrations.
22
22
  | `dogfood-runner.js` | Run deterministic messy-repo scenarios against migration, host, extension, and suite release behavior. |
23
23
  | `budget.js` | Read and enforce configured budget controls. |
24
24
  | `cost-tracker.js` | Track token and cost estimates from event streams. |
25
+ | `fs-async.js` | Promise-based file read/write helpers for non-blocking runtime paths. |
25
26
 
26
27
  ## Events and observability
27
28
 
@@ -43,6 +44,8 @@ package-level integrations.
43
44
  | `workflow-runner.js` | Execute workflow steps with validation hooks. |
44
45
  | `agent-cache.js` | Cache agent metadata for faster routing. |
45
46
  | `agent-validator.js` | Validate agent frontmatter and contracts. |
47
+ | `agent-refs.js` | Validate workflow agent references and scan skill/agent prose for phantom references. |
48
+ | `skill-surface.js` | Derive slash-command metadata from the individual `skills/` files. |
46
49
 
47
50
  ## Artifact quality
48
51
 
@@ -71,6 +74,7 @@ package-level integrations.
71
74
  | `impeccable-bridge.js` | Bridge runtime checks into impeccable quality workflows. |
72
75
  | `extensions.js` | Load and validate extension packs. |
73
76
  | `extension-authoring.js` | Scaffold publishable extension packs with manifest, package, README, skill, agent, and workflow files. |
77
+ | `pillars.js` | Manage the Pillars project-context layer (`AGENTS.md` plus routed `agents/*.md`). |
74
78
 
75
79
  ## Repository and graph helpers
76
80
 
@@ -87,5 +91,17 @@ package-level integrations.
87
91
  | `review-required.js` | Decide when review gates should block progress. |
88
92
  | `suite-state.js` | Manage state across registered project suites. |
89
93
 
94
+ ## Installer, dashboard, and CLI helpers
95
+
96
+ | Module | Purpose |
97
+ |--------|---------|
98
+ | `installer-core.js` | Install and uninstall the Godpowers surface for each runtime. |
99
+ | `installer-files.js` | File-copy helpers shared by the installer and its tests. |
100
+ | `installer-args.js` | Parse `bin/install.js` arguments and subcommands. |
101
+ | `installer-runtimes.js` | Map supported runtimes to their config directories. |
102
+ | `automation-providers.js` | Detect and configure host-native automation providers. |
103
+ | `dashboard.js` | Compute the next-step action brief and host guarantee line. |
104
+ | `quick-proof.js` | Render the shipped proof fixture for `godpowers quick-proof`. |
105
+
90
106
  See `../ARCHITECTURE.md` for system design and `../docs/ROADMAP.md` for planned
91
107
  runtime work.
@@ -30,7 +30,7 @@
30
30
  * isInstalled() -> bool
31
31
  */
32
32
 
33
- const { execSync } = require('child_process');
33
+ const { execFileSync } = require('child_process');
34
34
  const path = require('path');
35
35
 
36
36
  /**
@@ -38,14 +38,14 @@ const path = require('path');
38
38
  */
39
39
  function isInstalled() {
40
40
  try {
41
- execSync('agent-browser --version', {
41
+ execFileSync('agent-browser', ['--version'], {
42
42
  stdio: ['ignore', 'pipe', 'pipe'],
43
43
  timeout: 5000
44
44
  });
45
45
  return true;
46
46
  } catch (e1) {
47
47
  try {
48
- execSync('npx --no-install agent-browser --version', {
48
+ execFileSync('npx', ['--no-install', 'agent-browser', '--version'], {
49
49
  stdio: ['ignore', 'pipe', 'pipe'],
50
50
  timeout: 5000
51
51
  });
@@ -59,21 +59,21 @@ function isInstalled() {
59
59
  /**
60
60
  * Run an agent-browser CLI command and return stdout.
61
61
  * Throws on non-zero exit. Times out after 30s default.
62
+ *
63
+ * Arguments are passed as an argv array with the shell disabled. URLs,
64
+ * selectors, and eval expressions can originate from project content
65
+ * (PRD/DESIGN acceptance criteria, CLI flags), so they must never be
66
+ * concatenated into a shell command string where metacharacters like
67
+ * `;`, `&&`, `$()`, or backticks could trigger command injection.
62
68
  */
63
69
  function run(args, opts = {}) {
64
- const cmd = Array.isArray(args) ? args : [args];
65
- // Quote arguments containing spaces (best-effort)
66
- const argString = cmd.map(a => {
67
- const s = String(a);
68
- if (/\s/.test(s) && !s.startsWith('"')) return `"${s.replace(/"/g, '\\"')}"`;
69
- return s;
70
- }).join(' ');
71
- const fullCmd = `agent-browser ${argString}`;
72
- return execSync(fullCmd, {
70
+ const cmd = (Array.isArray(args) ? args : [args]).map(String);
71
+ return execFileSync('agent-browser', cmd, {
73
72
  stdio: ['ignore', 'pipe', 'pipe'],
74
73
  timeout: opts.timeout || 30000,
75
74
  cwd: opts.cwd || process.cwd(),
76
- encoding: 'utf8'
75
+ encoding: 'utf8',
76
+ shell: false
77
77
  });
78
78
  }
79
79
 
@@ -139,7 +139,14 @@ function clear(projectRoot, opts = {}) {
139
139
  const fpath = path.join(shardPath, fname);
140
140
  let entry;
141
141
  try { entry = JSON.parse(fs.readFileSync(fpath, 'utf8')); }
142
- catch (e) { fs.unlinkSync(fpath); removed++; continue; }
142
+ catch (e) {
143
+ // Corrupt or non-JSON entry: prune it only on an explicit full clear.
144
+ // A narrow clear (by agent, expiry, or age) must not delete unrelated
145
+ // unparseable files it was never asked to touch.
146
+ if (opts.all) { fs.unlinkSync(fpath); removed++; }
147
+ else { kept++; }
148
+ continue;
149
+ }
143
150
  let shouldRemove = false;
144
151
  if (opts.all) shouldRemove = true;
145
152
  else if (opts.agent && entry.agent === opts.agent) shouldRemove = true;
@@ -0,0 +1,161 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { isCompatible, parseManifest } = require('./extensions');
4
+
5
+ const AGENT_CONTRACT_VERSION = '1.0.0';
6
+
7
+ /**
8
+ * @typedef {Object} AgentRef
9
+ * @property {string|null} agent Agent name without the range suffix.
10
+ * @property {string|null} range SemVer range declared after the final at sign.
11
+ * @property {string} raw Original workflow `uses` value.
12
+ */
13
+
14
+ /**
15
+ * @typedef {AgentRef & { contractVersion: string, valid: boolean, errors: string[] }} AgentRefValidation
16
+ */
17
+
18
+ /**
19
+ * @param {string} ref
20
+ * @returns {AgentRef}
21
+ */
22
+ function parseAgentRef(ref) {
23
+ if (!ref) return { agent: null, range: null, raw: ref };
24
+ const raw = String(ref).trim();
25
+ const at = raw.lastIndexOf('@');
26
+ if (at <= 0) {
27
+ return { agent: raw, range: null, raw };
28
+ }
29
+ return {
30
+ agent: raw.slice(0, at),
31
+ range: raw.slice(at + 1),
32
+ raw
33
+ };
34
+ }
35
+
36
+ /**
37
+ * @param {string} ref
38
+ * @param {string} [contractVersion]
39
+ * @returns {AgentRefValidation}
40
+ */
41
+ function validateAgentRef(ref, contractVersion = AGENT_CONTRACT_VERSION) {
42
+ const parsed = parseAgentRef(ref);
43
+ const errors = [];
44
+
45
+ if (!parsed.agent || !/^[a-z][a-z0-9-]*$/.test(parsed.agent)) {
46
+ errors.push(`invalid agent name in uses: ${ref}`);
47
+ }
48
+ if (!parsed.range) {
49
+ errors.push(`agent ref must include a semver range: ${ref}`);
50
+ } else if (!isCompatible(parsed.range, contractVersion)) {
51
+ errors.push(`agent ref ${ref} does not satisfy agent contract ${contractVersion}`);
52
+ }
53
+
54
+ return { ...parsed, contractVersion, valid: errors.length === 0, errors };
55
+ }
56
+
57
+ /**
58
+ * @param {string} ref
59
+ * @param {string} [contractVersion]
60
+ * @returns {AgentRefValidation}
61
+ */
62
+ function assertAgentRef(ref, contractVersion = AGENT_CONTRACT_VERSION) {
63
+ const result = validateAgentRef(ref, contractVersion);
64
+ if (!result.valid) {
65
+ throw new Error(result.errors.join('; '));
66
+ }
67
+ return result;
68
+ }
69
+
70
+ /**
71
+ * @typedef {Object} ProseRef
72
+ * @property {string} token Unresolved god-* token (leading slash stripped).
73
+ * @property {string} file Repo-relative path where the token was found.
74
+ * @property {number} count Occurrences of that token in that file.
75
+ */
76
+
77
+ const PROSE_TOKEN = /(?<![A-Za-z0-9_])\/?god-[a-z0-9]+(?:-[a-z0-9]+)*/g;
78
+
79
+ /**
80
+ * Collect the set of every god-* command/agent name that legitimately exists:
81
+ * core skills, core agents, and the skills/agents provided by first-party
82
+ * extension packs. {god, godpowers} are always valid (front door + binary).
83
+ *
84
+ * @param {string} rootDir Repository root.
85
+ * @returns {Set<string>}
86
+ */
87
+ function knownGodNames(rootDir) {
88
+ const valid = new Set(['god', 'godpowers']);
89
+
90
+ for (const f of fs.readdirSync(path.join(rootDir, 'skills'))) {
91
+ if (f.endsWith('.md')) valid.add(f.replace(/\.md$/, ''));
92
+ }
93
+ for (const f of fs.readdirSync(path.join(rootDir, 'agents'))) {
94
+ if (/^god-.*\.md$/.test(f)) valid.add(f.replace(/\.md$/, ''));
95
+ }
96
+
97
+ const extDir = path.join(rootDir, 'extensions');
98
+ if (fs.existsSync(extDir)) {
99
+ for (const pack of fs.readdirSync(extDir)) {
100
+ const manifestPath = path.join(extDir, pack, 'manifest.yaml');
101
+ if (!fs.existsSync(manifestPath)) continue;
102
+ let provides = {};
103
+ try {
104
+ const parsed = parseManifest(fs.readFileSync(manifestPath, 'utf8'));
105
+ provides = (parsed && parsed.manifest && parsed.manifest.provides) || {};
106
+ } catch (e) {
107
+ continue;
108
+ }
109
+ for (const list of [provides.skills, provides.agents]) {
110
+ if (Array.isArray(list)) {
111
+ for (const name of list) valid.add(String(name).trim());
112
+ }
113
+ }
114
+ }
115
+ }
116
+
117
+ return valid;
118
+ }
119
+
120
+ /**
121
+ * Scan skill and agent prose for `god-*` references that do not resolve to a
122
+ * real core skill, core agent, or extension-provided skill/agent. Catches
123
+ * phantom references that the workflow `uses:` validator cannot see because
124
+ * they live in markdown bodies rather than YAML.
125
+ *
126
+ * @param {string} rootDir Repository root.
127
+ * @returns {ProseRef[]} Unresolved references (empty when everything resolves).
128
+ */
129
+ function findUnresolvedProseRefs(rootDir) {
130
+ const valid = knownGodNames(rootDir);
131
+ const unresolved = [];
132
+
133
+ for (const dir of ['skills', 'agents']) {
134
+ const dirPath = path.join(rootDir, dir);
135
+ for (const f of fs.readdirSync(dirPath)) {
136
+ if (!f.endsWith('.md')) continue;
137
+ const text = fs.readFileSync(path.join(dirPath, f), 'utf8');
138
+ const counts = new Map();
139
+ let m;
140
+ while ((m = PROSE_TOKEN.exec(text)) !== null) {
141
+ const token = m[0].replace(/^\//, '');
142
+ if (valid.has(token)) continue;
143
+ counts.set(token, (counts.get(token) || 0) + 1);
144
+ }
145
+ for (const [token, count] of counts) {
146
+ unresolved.push({ token, file: `${dir}/${f}`, count });
147
+ }
148
+ }
149
+ }
150
+
151
+ return unresolved;
152
+ }
153
+
154
+ module.exports = {
155
+ AGENT_CONTRACT_VERSION,
156
+ parseAgentRef,
157
+ validateAgentRef,
158
+ assertAgentRef,
159
+ knownGodNames,
160
+ findUnresolvedProseRefs
161
+ };
package/lib/budget.js CHANGED
@@ -75,19 +75,32 @@ function writeBlock(projectRoot, newBudgets) {
75
75
  */
76
76
  function stripBudgets(raw) {
77
77
  const lines = raw.split('\n');
78
- const out = [];
79
- let inBudgets = false;
80
- for (const line of lines) {
81
- if (/^budgets\s*:/.test(line)) { inBudgets = true; continue; }
82
- if (inBudgets) {
83
- // End of block when we hit a non-indented non-empty line
84
- if (/^\S/.test(line)) { inBudgets = false; out.push(line); continue; }
85
- // Skip block lines
86
- continue;
78
+ const range = findTopLevelBlock(lines, 'budgets');
79
+ if (!range) return raw;
80
+ const out = lines.slice(0, range.start).concat(lines.slice(range.end));
81
+ return out.join('\n').replace(/\n{3,}/g, '\n\n');
82
+ }
83
+
84
+ function findTopLevelBlock(lines, key) {
85
+ const header = new RegExp(`^${key}\\s*:(?:\\s*(?:#.*)?)?$`);
86
+ let start = -1;
87
+ for (let i = 0; i < lines.length; i++) {
88
+ if (header.test(lines[i])) {
89
+ start = i;
90
+ break;
87
91
  }
88
- out.push(line);
89
92
  }
90
- return out.join('\n').replace(/\n{3,}/g, '\n\n');
93
+ if (start === -1) return null;
94
+
95
+ let end = lines.length;
96
+ for (let i = start + 1; i < lines.length; i++) {
97
+ if (!lines[i].trim()) continue;
98
+ if (/^\S/.test(lines[i])) {
99
+ end = i;
100
+ break;
101
+ }
102
+ }
103
+ return { start, end };
91
104
  }
92
105
 
93
106
  /**
@@ -211,5 +224,6 @@ module.exports = {
211
224
  writeBlock,
212
225
  stripBudgets,
213
226
  renderBudgets,
227
+ findTopLevelBlock,
214
228
  DEFAULTS
215
229
  };
@@ -216,15 +216,21 @@ function writeFenced(filePath, sectionContent) {
216
216
  }
217
217
 
218
218
  /**
219
- * Remove the Godpowers fence from a file. Leaves the rest untouched. If the
220
- * file becomes empty (whitespace only), removes it.
219
+ * Remove the Godpowers fence from a file. Leaves the rest untouched. If only
220
+ * the fence remained, an auto-generated pointer file is deleted; with
221
+ * { preserveFile: true } the file is emptied instead of deleted (used for the
222
+ * canonical AGENTS.md so the off-switch never removes the user's primary file).
221
223
  */
222
- function removeFenced(filePath) {
224
+ function removeFenced(filePath, opts = {}) {
223
225
  if (!fs.existsSync(filePath)) return { removed: false, reason: 'missing' };
224
226
  const parsed = readFenced(filePath);
225
227
  if (parsed.fenced === '') return { removed: false, reason: 'no-fence' };
226
228
  let remaining = `${parsed.before}${parsed.after}`.replace(/\n{3,}/g, '\n\n').trim();
227
229
  if (remaining === '') {
230
+ if (opts.preserveFile) {
231
+ fs.writeFileSync(filePath, '');
232
+ return { removed: true, fileDeleted: false, emptied: true };
233
+ }
228
234
  fs.unlinkSync(filePath);
229
235
  return { removed: true, fileDeleted: true };
230
236
  }
@@ -284,8 +290,11 @@ function apply(projectRoot, state, opts = {}) {
284
290
  * Remove all Godpowers fences from canonical + pointers (off-switch).
285
291
  */
286
292
  function clearAll(projectRoot) {
287
- const targets = [
288
- path.join(projectRoot, 'AGENTS.md'),
293
+ // The canonical AGENTS.md is the user's primary context file: empty it rather
294
+ // than delete it. The remaining targets are auto-generated pointer files that
295
+ // Godpowers owns outright, so they are deleted when only the fence remains.
296
+ const canonical = path.join(projectRoot, 'AGENTS.md');
297
+ const pointers = [
289
298
  path.join(projectRoot, 'CLAUDE.md'),
290
299
  path.join(projectRoot, 'GEMINI.md'),
291
300
  path.join(projectRoot, '.cursorrules'),
@@ -300,7 +309,9 @@ function clearAll(projectRoot) {
300
309
  path.join(projectRoot, '.agents', 'skills', 'godpowers.md')
301
310
  ];
302
311
  const results = [];
303
- for (const t of targets) {
312
+ const rc = removeFenced(canonical, { preserveFile: true });
313
+ if (rc.removed) results.push({ path: canonical, ...rc });
314
+ for (const t of pointers) {
304
315
  const r = removeFenced(t);
305
316
  if (r.removed) results.push({ path: t, ...r });
306
317
  }
package/lib/events.js CHANGED
@@ -161,10 +161,17 @@ function verifyChain(file) {
161
161
  function readRun(projectRoot, runId) {
162
162
  const file = eventsPath(projectRoot, runId);
163
163
  if (!fs.existsSync(file)) return [];
164
- return fs.readFileSync(file, 'utf8')
165
- .split('\n')
166
- .filter(Boolean)
167
- .map(line => JSON.parse(line));
164
+ const out = [];
165
+ for (const line of fs.readFileSync(file, 'utf8').split('\n')) {
166
+ if (!line) continue;
167
+ try {
168
+ out.push(JSON.parse(line));
169
+ } catch (e) {
170
+ // Skip a torn or partial line (e.g. the process was killed mid-append).
171
+ // Hash-chain integrity is verified separately by verifyChain.
172
+ }
173
+ }
174
+ return out;
168
175
  }
169
176
 
170
177
  /**
@@ -26,6 +26,24 @@ function packageFolderName(name) {
26
26
  return name.replace(/^@/, '').replace(/\//g, '-');
27
27
  }
28
28
 
29
+ function assertSafePackageName(value) {
30
+ if (!/^@?[a-z0-9][a-z0-9-]*(\/[a-z0-9][a-z0-9-]*)?$/.test(value)) {
31
+ throw new Error(
32
+ `Invalid extension name "${value}": expected a package name like ` +
33
+ `"@scope/pack" or "pack" using lowercase letters, digits, and dashes.`
34
+ );
35
+ }
36
+ }
37
+
38
+ function assertSafeSegment(value, label) {
39
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(value)) {
40
+ throw new Error(
41
+ `Invalid ${label} "${value}": use lowercase letters, digits, and dashes ` +
42
+ `only. Slashes, dots, and ".." are rejected to prevent path traversal.`
43
+ );
44
+ }
45
+ }
46
+
29
47
  function scaffold(outputRoot, opts = {}) {
30
48
  const name = opts.name || '@godpowers/custom-pack';
31
49
  const version = opts.version || '0.1.0';
@@ -34,6 +52,15 @@ function scaffold(outputRoot, opts = {}) {
34
52
  const workflow = opts.workflow || null;
35
53
  const range = opts.godpowersRange || '>=1.6.0';
36
54
  const folder = opts.folder || packageFolderName(name);
55
+
56
+ // Validate all user-supplied names before they reach a filesystem path, so a
57
+ // value like "../../escape" can never write outside the output folder.
58
+ assertSafePackageName(name);
59
+ assertSafeSegment(skill, 'skill');
60
+ if (agent) assertSafeSegment(agent, 'agent');
61
+ if (workflow) assertSafeSegment(workflow, 'workflow');
62
+ assertSafeSegment(folder, 'folder');
63
+
37
64
  const root = path.join(outputRoot, folder);
38
65
  const written = [];
39
66
 
@@ -89,6 +89,24 @@ const FEATURES = [
89
89
  commands: ['godpowers quick-proof'],
90
90
  description: 'Render a shipped proof fixture with computed next action and host guarantees.'
91
91
  },
92
+ {
93
+ id: 'request-trace-review',
94
+ since: '2.0.1',
95
+ commands: ['/god-build', '/god-review', '/god-feature', '/god-refactor'],
96
+ description: 'Keep implementation diffs narrow by requiring request-trace evidence and reviewer checks for scope, simplicity, and surgicality.'
97
+ },
98
+ {
99
+ id: 'release-hardening',
100
+ since: '2.0.2',
101
+ commands: ['npm test', 'npm run lint', 'npm run release:check'],
102
+ description: 'Maintain release validation through a delegated test runner, static checks, parser coverage, and hardened package and runtime checks.'
103
+ },
104
+ {
105
+ id: 'maintenance-hardening',
106
+ since: '2.0.3',
107
+ commands: ['npm test', 'npm run lint', 'npm run release:check'],
108
+ description: 'Track installer decomposition, shared test harness adoption, executable agent refs, skill metadata sync, God Mode runbook delegation, and async runtime APIs.'
109
+ },
92
110
  {
93
111
  id: 'extension-authoring',
94
112
  since: '1.6.22',
@@ -0,0 +1,28 @@
1
+ const fs = require('fs/promises');
2
+ const path = require('path');
3
+
4
+ async function exists(filePath) {
5
+ try {
6
+ await fs.access(filePath);
7
+ return true;
8
+ } catch (_) {
9
+ return false;
10
+ }
11
+ }
12
+
13
+ async function readJson(filePath) {
14
+ return JSON.parse(await fs.readFile(filePath, 'utf8'));
15
+ }
16
+
17
+ async function writeJson(filePath, value) {
18
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
19
+ await fs.writeFile(filePath, JSON.stringify(value, null, 2) + '\n');
20
+ return value;
21
+ }
22
+
23
+ module.exports = {
24
+ exists,
25
+ readJson,
26
+ writeJson,
27
+ fs
28
+ };
@@ -0,0 +1,99 @@
1
+ const path = require('path');
2
+ const { RUNTIMES } = require('./installer-runtimes');
3
+
4
+ const COMMANDS = new Set([
5
+ 'status',
6
+ 'next',
7
+ 'quick-proof',
8
+ 'automation-status',
9
+ 'automation-setup',
10
+ 'dogfood',
11
+ 'extension-scaffold'
12
+ ]);
13
+
14
+ function parseArgs(argv, cwd = process.cwd()) {
15
+ const args = argv.slice(2);
16
+ const opts = {
17
+ command: null,
18
+ project: cwd,
19
+ json: false,
20
+ brief: false,
21
+ extensionName: null,
22
+ extensionOutput: cwd,
23
+ extensionSkill: null,
24
+ extensionAgent: null,
25
+ extensionWorkflow: null,
26
+ runtimes: [],
27
+ global: false,
28
+ local: false,
29
+ all: false,
30
+ help: false,
31
+ uninstall: false,
32
+ };
33
+
34
+ for (let i = 0; i < args.length; i++) {
35
+ const arg = args[i];
36
+ if (COMMANDS.has(arg)) {
37
+ opts.command = arg;
38
+ continue;
39
+ }
40
+
41
+ switch (arg) {
42
+ case '--json':
43
+ opts.json = true;
44
+ break;
45
+ case '--brief':
46
+ opts.brief = true;
47
+ break;
48
+ case '--project':
49
+ if (args[i + 1]) {
50
+ opts.project = path.resolve(args[i + 1]);
51
+ i++;
52
+ }
53
+ break;
54
+ case '-g':
55
+ case '--global':
56
+ opts.global = true;
57
+ break;
58
+ case '-l':
59
+ case '--local':
60
+ opts.local = true;
61
+ break;
62
+ case '--all':
63
+ opts.all = true;
64
+ break;
65
+ case '-h':
66
+ case '--help':
67
+ opts.help = true;
68
+ break;
69
+ case '-u':
70
+ case '--uninstall':
71
+ opts.uninstall = true;
72
+ break;
73
+ default:
74
+ if (arg.startsWith('--project=')) {
75
+ opts.project = path.resolve(arg.slice('--project='.length));
76
+ } else if (arg.startsWith('--name=')) {
77
+ opts.extensionName = arg.slice('--name='.length);
78
+ } else if (arg.startsWith('--output=')) {
79
+ opts.extensionOutput = path.resolve(arg.slice('--output='.length));
80
+ } else if (arg.startsWith('--skill=')) {
81
+ opts.extensionSkill = arg.slice('--skill='.length);
82
+ } else if (arg.startsWith('--agent=')) {
83
+ opts.extensionAgent = arg.slice('--agent='.length);
84
+ } else if (arg.startsWith('--workflow=')) {
85
+ opts.extensionWorkflow = arg.slice('--workflow='.length);
86
+ } else if (arg.startsWith('--') && RUNTIMES[arg.slice(2)]) {
87
+ opts.runtimes.push(arg.slice(2));
88
+ }
89
+ break;
90
+ }
91
+ }
92
+
93
+ return opts;
94
+ }
95
+
96
+ module.exports = {
97
+ COMMANDS,
98
+ parseArgs
99
+ };