@agnostic-prompt/aps 1.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.
- package/LICENSE +190 -0
- package/README.md +38 -0
- package/bin/aps.js +8 -0
- package/package.json +35 -0
- package/payload/agnostic-prompt-standard/SKILL.md +73 -0
- package/payload/agnostic-prompt-standard/assets/agents/vscode-agent-v1.0.0.agent.md +187 -0
- package/payload/agnostic-prompt-standard/assets/constants/constants-json-block-v1.0.0.example.md +15 -0
- package/payload/agnostic-prompt-standard/assets/constants/constants-text-block-v1.0.0.example.md +15 -0
- package/payload/agnostic-prompt-standard/assets/formats/format-code-changes-full-v1.0.0.example.md +21 -0
- package/payload/agnostic-prompt-standard/assets/formats/format-code-map-v1.0.0.example.md +18 -0
- package/payload/agnostic-prompt-standard/assets/formats/format-error-v1.0.0.example.md +9 -0
- package/payload/agnostic-prompt-standard/assets/formats/format-hierarchical-outline-v1.0.0.example.md +17 -0
- package/payload/agnostic-prompt-standard/assets/formats/format-ideation-list-v1.0.0.example.md +20 -0
- package/payload/agnostic-prompt-standard/assets/formats/format-markdown-table-v1.0.0.example.md +17 -0
- package/payload/agnostic-prompt-standard/assets/formats/format-table-api-coverage-v1.0.0.example.md +13 -0
- package/payload/agnostic-prompt-standard/platforms/README.md +39 -0
- package/payload/agnostic-prompt-standard/platforms/_schemas/platform-manifest.schema.json +84 -0
- package/payload/agnostic-prompt-standard/platforms/_schemas/tools-registry.schema.json +148 -0
- package/payload/agnostic-prompt-standard/platforms/_template/README.md +10 -0
- package/payload/agnostic-prompt-standard/platforms/_template/manifest.json +25 -0
- package/payload/agnostic-prompt-standard/platforms/_template/tools-registry.json +12 -0
- package/payload/agnostic-prompt-standard/platforms/vscode-copilot/README.md +100 -0
- package/payload/agnostic-prompt-standard/platforms/vscode-copilot/frontmatter/agent-frontmatter.md +24 -0
- package/payload/agnostic-prompt-standard/platforms/vscode-copilot/frontmatter/instructions-frontmatter.md +12 -0
- package/payload/agnostic-prompt-standard/platforms/vscode-copilot/frontmatter/prompt-frontmatter.md +18 -0
- package/payload/agnostic-prompt-standard/platforms/vscode-copilot/frontmatter/skill-frontmatter.md +10 -0
- package/payload/agnostic-prompt-standard/platforms/vscode-copilot/manifest.json +40 -0
- package/payload/agnostic-prompt-standard/platforms/vscode-copilot/templates/.github/agents/aps-prompt-protocol.agent.md +187 -0
- package/payload/agnostic-prompt-standard/platforms/vscode-copilot/templates/AGENTS.md +7 -0
- package/payload/agnostic-prompt-standard/platforms/vscode-copilot/tools-registry.json +509 -0
- package/payload/agnostic-prompt-standard/references/00-structure.md +123 -0
- package/payload/agnostic-prompt-standard/references/01-vocabulary.md +124 -0
- package/payload/agnostic-prompt-standard/references/02-linting-and-formatting.md +122 -0
- package/payload/agnostic-prompt-standard/references/03-agentic-control.md +192 -0
- package/payload/agnostic-prompt-standard/references/04-schemas-and-types.md +137 -0
- package/payload/agnostic-prompt-standard/references/05-grammar.md +99 -0
- package/payload/agnostic-prompt-standard/references/06-logging-and-privacy.md +32 -0
- package/payload/agnostic-prompt-standard/references/07-error-taxonomy.md +62 -0
- package/payload/agnostic-prompt-standard/scripts/.gitkeep +0 -0
- package/scripts/lint.js +27 -0
- package/scripts/prepack.js +23 -0
- package/src/cli.js +292 -0
- package/src/core.js +179 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# 06 Logging and privacy
|
|
2
|
+
|
|
3
|
+
This document defines the normative logging and redaction requirements for APS executors.
|
|
4
|
+
|
|
5
|
+
## Logging
|
|
6
|
+
|
|
7
|
+
```yaml
|
|
8
|
+
logging:
|
|
9
|
+
capture_points: [RUN, USE, CAPTURE, SET, UNSET, ASSERT, RETURN, PAR, JOIN, TELL, SNAP]
|
|
10
|
+
include:
|
|
11
|
+
- timestamp
|
|
12
|
+
- process_id
|
|
13
|
+
- step_index
|
|
14
|
+
- action
|
|
15
|
+
- inputs
|
|
16
|
+
- outputs
|
|
17
|
+
- artifacts
|
|
18
|
+
- prior_hash
|
|
19
|
+
- new_hash
|
|
20
|
+
- origin
|
|
21
|
+
- policy_hash
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Redaction
|
|
25
|
+
|
|
26
|
+
Engines MUST enforce:
|
|
27
|
+
|
|
28
|
+
- Secrets/PII MUST be redacted as `[REDACTED]` (`AG-032`).
|
|
29
|
+
- For `SNAP` with `redact=[SYMS]`, engines MUST zeroize the listed symbols in `prior_state`,
|
|
30
|
+
`new_state`, and `artifacts`.
|
|
31
|
+
- If `TELL` uses `why:SYMBOL` and `SYMBOL` is redacted, only the symbol name may appear; its
|
|
32
|
+
content MUST NOT.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# 07 Error taxonomy
|
|
2
|
+
|
|
3
|
+
This document defines the normative error and warning codes.
|
|
4
|
+
|
|
5
|
+
## Errors
|
|
6
|
+
|
|
7
|
+
```yaml
|
|
8
|
+
errors:
|
|
9
|
+
hard:
|
|
10
|
+
- { code: AG-001, name: UndefinedSymbol, desc: Symbol not defined in <constants> or <runtime>. }
|
|
11
|
+
- { code: AG-002, name: ReservedTokenMisuse, desc: Reserved word used as ID/Key/Symbol. }
|
|
12
|
+
- { code: AG-003, name: InvalidId, desc: Process/tool/key not matching naming regex. }
|
|
13
|
+
- { code: AG-004, name: ProcessIdMismatch, desc: RUN references missing <process id="…">. }
|
|
14
|
+
- { code: AG-006, name: UnresolvedPlaceholder, desc: Placeholder could not be resolved. }
|
|
15
|
+
- { code: AG-007, name: BadJSON, desc: Invalid JSON value or pair. }
|
|
16
|
+
- { code: AG-008, name: CaptureMissing, desc: CAPTURE references unknown/never-executed tool. }
|
|
17
|
+
- { code: AG-009, name: TagMismatch, desc: Unbalanced or wrong closing tag. }
|
|
18
|
+
- { code: AG-010, name: CommentDetected, desc: Comment present in executable blocks. }
|
|
19
|
+
- { code: AG-011, name: TabDetected, desc: Tab characters present. }
|
|
20
|
+
- { code: AG-012, name: KeyOrder, desc: Keys in where: not lexicographic. }
|
|
21
|
+
- { code: AG-013, name: DuplicateSymbol, desc: Symbol redefined with incompatible type/origin. }
|
|
22
|
+
- { code: AG-014, name: TimeFormat, desc: Non-ISO 8601 time/offset where required. }
|
|
23
|
+
- { code: AG-015, name: CasePolicy, desc: Non-lowercase booleans or non-double-quoted strings. }
|
|
24
|
+
- { code: AG-016, name: ProcessNameAttrMismatch, desc: <process> Name attr missing/malformed. }
|
|
25
|
+
- { code: AG-017, name: ToolPolicy, desc: Tools used in <triggers>. }
|
|
26
|
+
- { code: AG-018, name: ConcurrencyPolicy, desc: PAR/JOIN misuse or nondeterministic ordering. }
|
|
27
|
+
- { code: AG-019, name: ForbiddenSymbolOrigin, desc: SET origin missing/invalid. }
|
|
28
|
+
- { code: AG-021, name: STEValidationFailed, desc: ste=true text failed STE lints. }
|
|
29
|
+
- { code: AG-022, name: RandomnessPolicy, desc: Randomness used without seed where policy forbids. }
|
|
30
|
+
- { code: AG-023, name: WithScopeError, desc: WITH defaults malformed or leaked across scope boundary. }
|
|
31
|
+
- { code: AG-024, name: AliasMapError, desc: ALIAS mapping invalid or collides with symbol names. }
|
|
32
|
+
- { code: AG-027, name: TimeoutRetryPolicy, desc: timeout_ms/retry invalid type/range. }
|
|
33
|
+
- { code: AG-028, name: CapturePathError, desc: CAPTURE map path invalid or type coercion failed. }
|
|
34
|
+
- { code: AG-029, name: AssertInvalid, desc: ASSERT expression invalid or unsafely side-effecting. }
|
|
35
|
+
- { code: AG-030, name: SemicolonDetected, desc: Semicolon ';' used where newline termination is required. }
|
|
36
|
+
- { code: AG-031, name: PaddingWhitespace, desc: Excess inter-token spaces detected; exactly one ASCII space required in compiled form. }
|
|
37
|
+
- { code: AG-032, name: SensitiveInLog, desc: Secrets/PII leaked in logs or errors. }
|
|
38
|
+
- { code: AG-033, name: InstructionsLinePolicy, desc: Multiple sentences per line, blank lines, or non-directive lines present in <instructions>. }
|
|
39
|
+
- { code: AG-034, name: PredefinedToolCollision, desc: Conflicting tool signatures across host and predefinedTools.json. }
|
|
40
|
+
- { code: AG-035, name: InPromptConfigOrImports, desc: Presence of <config> or <import> tags in prompt. }
|
|
41
|
+
- { code: AG-036, name: FormatContractViolation, desc: Output does not match the referenced <format id="…"> template (missing headers/columns/markers/placeholders not resolved). }
|
|
42
|
+
- { code: AG-037, name: DictReferenceForbidden, desc: DICT-style reference @"…" used; constants must be defined in <constants>. }
|
|
43
|
+
- { code: AG-038, name: DictInConfigForbidden, desc: config.json contains a DICT key; migrate constants to <constants>. }
|
|
44
|
+
- { code: AG-039, name: FormatUndefined, desc: A step references a format id that is not defined in <formats>. }
|
|
45
|
+
- { code: AG-040, name: FormatFenceError, desc: Missing or malformed ```format:<ID> fenced block; multiple format blocks or surrounding prose where a single block is required. }
|
|
46
|
+
- { code: AG-041, name: FormatWhereMissing, desc: WHERE: section missing or not uppercase when placeholders are present (or required by policy). }
|
|
47
|
+
- { code: AG-042, name: PlaceholderMismatch, desc: Placeholder appears in body but not in WHERE, or defined in WHERE but not present in body. }
|
|
48
|
+
- { code: AG-043, name: PlaceholderStyleError, desc: Placeholder not in <UPPER_SNAKE> form or not wrapped in angle brackets. }
|
|
49
|
+
- { code: AG-044, name: ProcessArgsMismatch, desc: RUN statement arguments do not match the target process signature (missing, extra, or type-incompatible arguments). }
|
|
50
|
+
- { code: AG-045, name: BlockConstantUnterminated, desc: Block constant missing closing delimiter line >>. }
|
|
51
|
+
- { code: AG-046, name: BlockConstantTypeUnknown, desc: Block constant uses unknown <BLOCK_TYPE>; expected JSON or TEXT. }
|
|
52
|
+
|
|
53
|
+
warnings:
|
|
54
|
+
- { code: AG-W01, name: SymbolNotUsed, desc: Defined but never used. }
|
|
55
|
+
- { code: AG-W02, name: LaxTime, desc: Step without explicit time where policy requires. }
|
|
56
|
+
- { code: AG-W03, name: HeuristicInference, desc: Placeholder resolved by Agent Inference under strict policy. }
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Required behaviors
|
|
60
|
+
|
|
61
|
+
- Engines MUST treat all `errors.hard` codes as fatal for the current compile/run.
|
|
62
|
+
- Engines MAY continue on `warnings`, but MUST surface them to the caller.
|
|
File without changes
|
package/scripts/lint.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { readdirSync, statSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
function walk(dir) {
|
|
7
|
+
const out = [];
|
|
8
|
+
for (const entry of readdirSync(dir)) {
|
|
9
|
+
if (entry === 'payload') continue; // generated
|
|
10
|
+
const p = join(dir, entry);
|
|
11
|
+
const st = statSync(p);
|
|
12
|
+
if (st.isDirectory()) out.push(...walk(p));
|
|
13
|
+
else if (st.isFile() && p.endsWith('.js')) out.push(p);
|
|
14
|
+
}
|
|
15
|
+
return out;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const rootPath = fileURLToPath(new URL('..', import.meta.url));
|
|
19
|
+
|
|
20
|
+
const files = walk(rootPath);
|
|
21
|
+
let failed = false;
|
|
22
|
+
for (const file of files) {
|
|
23
|
+
const r = spawnSync(process.execPath, ['--check', file], { stdio: 'inherit' });
|
|
24
|
+
if (r.status !== 0) failed = true;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
process.exit(failed ? 1 : 0);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const pkgRoot = path.resolve(here, '..');
|
|
7
|
+
const repoRoot = path.resolve(pkgRoot, '..', '..');
|
|
8
|
+
|
|
9
|
+
const src = path.join(repoRoot, 'skill', 'agnostic-prompt-standard');
|
|
10
|
+
const dst = path.join(pkgRoot, 'payload', 'agnostic-prompt-standard');
|
|
11
|
+
|
|
12
|
+
async function main() {
|
|
13
|
+
// Clean target
|
|
14
|
+
await fs.rm(dst, { recursive: true, force: true });
|
|
15
|
+
await fs.mkdir(path.dirname(dst), { recursive: true });
|
|
16
|
+
await fs.cp(src, dst, { recursive: true });
|
|
17
|
+
console.log(`Synced APS skill payload -> ${dst}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
main().catch((err) => {
|
|
21
|
+
console.error(err);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
});
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import process from 'node:process';
|
|
4
|
+
import {
|
|
5
|
+
checkbox,
|
|
6
|
+
confirm,
|
|
7
|
+
input,
|
|
8
|
+
select,
|
|
9
|
+
} from '@inquirer/prompts';
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
SKILL_ID,
|
|
13
|
+
copyDir,
|
|
14
|
+
copyTemplates,
|
|
15
|
+
defaultPersonalSkillPath,
|
|
16
|
+
defaultProjectSkillPath,
|
|
17
|
+
ensureDir,
|
|
18
|
+
findRepoRoot,
|
|
19
|
+
inferPlatformId,
|
|
20
|
+
isDirectory,
|
|
21
|
+
pathExists,
|
|
22
|
+
removeDir,
|
|
23
|
+
resolvePayloadSkillDir,
|
|
24
|
+
loadPlatforms,
|
|
25
|
+
} from './core.js';
|
|
26
|
+
|
|
27
|
+
const EXIT_USAGE = 2;
|
|
28
|
+
|
|
29
|
+
function isTTY() {
|
|
30
|
+
return Boolean(process.stdout.isTTY && process.stdin.isTTY);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function normalizePlatform(value) {
|
|
34
|
+
if (!value) return undefined;
|
|
35
|
+
if (value === 'none') return null;
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function pickWorkspaceRoot(cliRoot) {
|
|
40
|
+
if (cliRoot) return path.resolve(cliRoot);
|
|
41
|
+
const repo = await findRepoRoot(process.cwd());
|
|
42
|
+
return repo;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function fmtPath(p) {
|
|
46
|
+
return p.replace(process.env.HOME ?? '', '~');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function runInit(options) {
|
|
50
|
+
const payloadSkillDir = await resolvePayloadSkillDir();
|
|
51
|
+
const repoRoot = await findRepoRoot(process.cwd());
|
|
52
|
+
const guessedWorkspaceRoot = await pickWorkspaceRoot(options.root);
|
|
53
|
+
|
|
54
|
+
// Determine scope
|
|
55
|
+
let installScope = options.personal ? 'personal' : options.repo ? 'repo' : undefined;
|
|
56
|
+
|
|
57
|
+
// Determine workspace root
|
|
58
|
+
let workspaceRoot = guessedWorkspaceRoot;
|
|
59
|
+
|
|
60
|
+
// Determine platform
|
|
61
|
+
const detectedPlatform = workspaceRoot ? inferPlatformId(workspaceRoot) : null;
|
|
62
|
+
let platformId = normalizePlatform(options.platform) ?? undefined;
|
|
63
|
+
|
|
64
|
+
// Determine extras
|
|
65
|
+
let installTemplates = Boolean(options.templates);
|
|
66
|
+
|
|
67
|
+
if (!options.yes && isTTY()) {
|
|
68
|
+
// Scope prompt
|
|
69
|
+
if (!installScope) {
|
|
70
|
+
installScope = await select({
|
|
71
|
+
message: 'Where should APS be installed?',
|
|
72
|
+
default: repoRoot ? 'repo' : 'personal',
|
|
73
|
+
choices: [
|
|
74
|
+
{
|
|
75
|
+
name: repoRoot ? `Project skill in this repo (${fmtPath(repoRoot)})` : 'Project skill (choose a workspace folder)',
|
|
76
|
+
value: 'repo',
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: `Personal skill for your user (${fmtPath(defaultPersonalSkillPath({ claude: options.claude }).replace(SKILL_ID, ''))})`,
|
|
80
|
+
value: 'personal',
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (installScope === 'repo' && !workspaceRoot) {
|
|
87
|
+
const rootAnswer = await input({
|
|
88
|
+
message: 'Workspace root path (the folder that contains .github/):',
|
|
89
|
+
default: process.cwd(),
|
|
90
|
+
});
|
|
91
|
+
workspaceRoot = path.resolve(rootAnswer);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!platformId && workspaceRoot) {
|
|
95
|
+
const platforms = await loadPlatforms(payloadSkillDir);
|
|
96
|
+
const choices = [
|
|
97
|
+
...(detectedPlatform ? [{ name: `Auto-detected: ${detectedPlatform}`, value: detectedPlatform }] : []),
|
|
98
|
+
{ name: 'None (skip platform templates)', value: null },
|
|
99
|
+
...platforms
|
|
100
|
+
.filter((p) => p.platformId !== detectedPlatform)
|
|
101
|
+
.map((p) => ({
|
|
102
|
+
name: `${p.displayName} (${p.platformId})`,
|
|
103
|
+
value: p.platformId,
|
|
104
|
+
})),
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
platformId = await select({
|
|
108
|
+
message: 'Select a platform adapter to apply:',
|
|
109
|
+
default: detectedPlatform ?? null,
|
|
110
|
+
choices,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (platformId && workspaceRoot) {
|
|
115
|
+
const extras = await checkbox({
|
|
116
|
+
message: 'Extras to apply to the workspace:',
|
|
117
|
+
choices: [
|
|
118
|
+
{
|
|
119
|
+
name: 'Install platform templates (e.g. VS Code agent file + AGENTS.md)',
|
|
120
|
+
value: 'templates',
|
|
121
|
+
checked: true,
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
});
|
|
125
|
+
installTemplates = extras.includes('templates');
|
|
126
|
+
}
|
|
127
|
+
} else {
|
|
128
|
+
// Non-interactive defaults
|
|
129
|
+
if (!installScope) installScope = repoRoot ? 'repo' : 'personal';
|
|
130
|
+
if (!platformId && workspaceRoot) platformId = detectedPlatform;
|
|
131
|
+
if (platformId && !options.templates) installTemplates = true;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Compute destinations
|
|
135
|
+
let skillDest;
|
|
136
|
+
if (installScope === 'repo') {
|
|
137
|
+
if (!workspaceRoot) {
|
|
138
|
+
throw new Error('Repo install selected but no workspace root found. Run in a git repo or pass --root <path>.');
|
|
139
|
+
}
|
|
140
|
+
skillDest = defaultProjectSkillPath(workspaceRoot, { claude: options.claude });
|
|
141
|
+
} else {
|
|
142
|
+
skillDest = defaultPersonalSkillPath({ claude: options.claude });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Preflight
|
|
146
|
+
const actions = [];
|
|
147
|
+
actions.push({ kind: 'skill', from: payloadSkillDir, to: skillDest });
|
|
148
|
+
|
|
149
|
+
let templatesDir = null;
|
|
150
|
+
if (platformId && installTemplates) {
|
|
151
|
+
templatesDir = path.join(payloadSkillDir, 'platforms', platformId, 'templates');
|
|
152
|
+
if (!(await isDirectory(templatesDir))) {
|
|
153
|
+
templatesDir = null;
|
|
154
|
+
} else if (!workspaceRoot) {
|
|
155
|
+
// If personal scope but we still want templates, default to current directory.
|
|
156
|
+
workspaceRoot = process.cwd();
|
|
157
|
+
}
|
|
158
|
+
if (templatesDir && workspaceRoot) {
|
|
159
|
+
actions.push({ kind: 'templates', from: templatesDir, to: workspaceRoot });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (options.dryRun) {
|
|
164
|
+
console.log('Dry run — planned actions:');
|
|
165
|
+
for (const a of actions) {
|
|
166
|
+
console.log(`- ${a.kind}: ${a.from} -> ${a.to}`);
|
|
167
|
+
}
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Execute
|
|
172
|
+
for (const a of actions) {
|
|
173
|
+
const destExists = await pathExists(a.to);
|
|
174
|
+
if (destExists && !options.force) {
|
|
175
|
+
if (!options.yes && isTTY()) {
|
|
176
|
+
const ok = await confirm({
|
|
177
|
+
message: `Destination already exists: ${a.to}\nOverwrite?`,
|
|
178
|
+
default: false,
|
|
179
|
+
});
|
|
180
|
+
if (!ok) {
|
|
181
|
+
console.log('Cancelled.');
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
} else {
|
|
185
|
+
throw new Error(`Destination exists: ${a.to} (use --force to overwrite)`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (destExists && options.force) {
|
|
190
|
+
await removeDir(a.to);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
await ensureDir(path.dirname(a.to));
|
|
194
|
+
|
|
195
|
+
if (a.kind === 'skill') {
|
|
196
|
+
await copyDir(a.from, a.to);
|
|
197
|
+
console.log(`Installed APS skill -> ${a.to}`);
|
|
198
|
+
} else if (a.kind === 'templates') {
|
|
199
|
+
await copyTemplates(a.from, a.to, { force: options.force });
|
|
200
|
+
console.log(`Applied platform templates -> ${a.to}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
console.log('\nNext steps (VS Code):');
|
|
205
|
+
console.log('- Ensure VS Code has Agent Skills + instruction files enabled as needed.');
|
|
206
|
+
console.log(`- Skill location: ${skillDest}`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function runDoctor(options) {
|
|
210
|
+
const root = await pickWorkspaceRoot(options.root);
|
|
211
|
+
const detected = root ? inferPlatformId(root) : null;
|
|
212
|
+
|
|
213
|
+
const rows = [];
|
|
214
|
+
|
|
215
|
+
if (root) {
|
|
216
|
+
const repoSkill = defaultProjectSkillPath(root, { claude: false });
|
|
217
|
+
const repoSkillClaude = defaultProjectSkillPath(root, { claude: true });
|
|
218
|
+
rows.push(['repo', repoSkill, await pathExists(path.join(repoSkill, 'SKILL.md'))]);
|
|
219
|
+
rows.push(['repo (claude)', repoSkillClaude, await pathExists(path.join(repoSkillClaude, 'SKILL.md'))]);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const personalSkill = defaultPersonalSkillPath({ claude: false });
|
|
223
|
+
const personalSkillClaude = defaultPersonalSkillPath({ claude: true });
|
|
224
|
+
rows.push(['personal', personalSkill, await pathExists(path.join(personalSkill, 'SKILL.md'))]);
|
|
225
|
+
rows.push(['personal (claude)', personalSkillClaude, await pathExists(path.join(personalSkillClaude, 'SKILL.md'))]);
|
|
226
|
+
|
|
227
|
+
console.log('APS Doctor');
|
|
228
|
+
console.log('----------');
|
|
229
|
+
console.log(`Workspace root: ${root ?? '(not detected)'}`);
|
|
230
|
+
console.log(`Detected platform: ${detected ?? '(none)'}`);
|
|
231
|
+
console.log('');
|
|
232
|
+
console.log('Installed skills:');
|
|
233
|
+
for (const [scope, p, ok] of rows) {
|
|
234
|
+
console.log(`- ${scope}: ${p} ${ok ? '✓' : '✗'}`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export async function main(argv) {
|
|
239
|
+
const program = new Command();
|
|
240
|
+
program
|
|
241
|
+
.name('aps')
|
|
242
|
+
.description('Install and manage the Agnostic Prompt Standard (APS) skill.')
|
|
243
|
+
.version('1.1.1');
|
|
244
|
+
|
|
245
|
+
program
|
|
246
|
+
.command('init')
|
|
247
|
+
.description('Install APS skill + (optionally) platform templates')
|
|
248
|
+
.option('--root <path>', 'Workspace root path (defaults to git repo root if found)')
|
|
249
|
+
.option('--repo', 'Install as a project skill (under .github/skills)')
|
|
250
|
+
.option('--personal', 'Install as a personal skill (under ~/.copilot/skills)')
|
|
251
|
+
.option('--platform <id>', 'Platform adapter to apply (e.g. vscode-copilot). Use "none" to skip.')
|
|
252
|
+
.option('--templates', 'Apply platform templates (default true when a platform is selected)')
|
|
253
|
+
.option('--claude', 'Use Claude platform .claude/skills paths instead of .github/skills and ~/.copilot/skills', false)
|
|
254
|
+
.option('-y, --yes', 'Non-interactive; accept defaults', false)
|
|
255
|
+
.option('-f, --force', 'Overwrite existing files', false)
|
|
256
|
+
.option('--dry-run', 'Print planned actions without writing', false)
|
|
257
|
+
.action((opts) => runInit(opts).catch((e) => {
|
|
258
|
+
console.error(`Error: ${e.message ?? e}`);
|
|
259
|
+
process.exit(1);
|
|
260
|
+
}));
|
|
261
|
+
|
|
262
|
+
program
|
|
263
|
+
.command('doctor')
|
|
264
|
+
.description('Check APS installation status + basic platform detection')
|
|
265
|
+
.option('--root <path>', 'Workspace root path (defaults to git repo root if found)')
|
|
266
|
+
.action((opts) => runDoctor(opts).catch((e) => {
|
|
267
|
+
console.error(`Error: ${e.message ?? e}`);
|
|
268
|
+
process.exit(1);
|
|
269
|
+
}));
|
|
270
|
+
|
|
271
|
+
program
|
|
272
|
+
.command('info')
|
|
273
|
+
.description('Print bundled APS payload info')
|
|
274
|
+
.action(async () => {
|
|
275
|
+
const payloadSkillDir = await resolvePayloadSkillDir();
|
|
276
|
+
const platforms = await loadPlatforms(payloadSkillDir);
|
|
277
|
+
console.log(`CLI version: 1.1.1`);
|
|
278
|
+
console.log(`Bundled skill path: ${payloadSkillDir}`);
|
|
279
|
+
console.log('Platforms:');
|
|
280
|
+
for (const p of platforms) {
|
|
281
|
+
console.log(`- ${p.platformId}: ${p.displayName} (templates: ${p.hasTemplates ? 'yes' : 'no'})`);
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
await program.parseAsync(argv);
|
|
286
|
+
|
|
287
|
+
// If no command was provided, show help and exit with usage.
|
|
288
|
+
if (!program.args.length) {
|
|
289
|
+
program.help({ error: true });
|
|
290
|
+
process.exit(EXIT_USAGE);
|
|
291
|
+
}
|
|
292
|
+
}
|
package/src/core.js
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
|
|
7
|
+
export const SKILL_ID = 'agnostic-prompt-standard';
|
|
8
|
+
|
|
9
|
+
export function homeDir() {
|
|
10
|
+
return os.homedir();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function pathExists(p) {
|
|
14
|
+
try {
|
|
15
|
+
await fs.access(p);
|
|
16
|
+
return true;
|
|
17
|
+
} catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function isDirectory(p) {
|
|
23
|
+
try {
|
|
24
|
+
const st = await fs.stat(p);
|
|
25
|
+
return st.isDirectory();
|
|
26
|
+
} catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function findRepoRoot(startDir) {
|
|
32
|
+
let cur = path.resolve(startDir);
|
|
33
|
+
while (true) {
|
|
34
|
+
const gitDir = path.join(cur, '.git');
|
|
35
|
+
if (existsSync(gitDir)) return cur;
|
|
36
|
+
const parent = path.dirname(cur);
|
|
37
|
+
if (parent === cur) return null;
|
|
38
|
+
cur = parent;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function defaultProjectSkillPath(repoRoot, opts = {}) {
|
|
43
|
+
const claude = Boolean(opts.claude);
|
|
44
|
+
return claude
|
|
45
|
+
? path.join(repoRoot, '.claude', 'skills', SKILL_ID)
|
|
46
|
+
: path.join(repoRoot, '.github', 'skills', SKILL_ID);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function defaultPersonalSkillPath(opts = {}) {
|
|
50
|
+
const claude = Boolean(opts.claude);
|
|
51
|
+
return claude
|
|
52
|
+
? path.join(homeDir(), '.claude', 'skills', SKILL_ID)
|
|
53
|
+
: path.join(homeDir(), '.copilot', 'skills', SKILL_ID);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function inferPlatformId(workspaceRoot) {
|
|
57
|
+
// Heuristic: VS Code + Copilot convention folders/files.
|
|
58
|
+
// If more platforms are added, extend this with adapter-specific heuristics.
|
|
59
|
+
const gh = path.join(workspaceRoot, '.github');
|
|
60
|
+
const hasAgents = existsSync(path.join(gh, 'agents'));
|
|
61
|
+
const hasPrompts = existsSync(path.join(gh, 'prompts'));
|
|
62
|
+
const hasInstructions = existsSync(path.join(gh, 'copilot-instructions.md')) || existsSync(path.join(gh, 'instructions'));
|
|
63
|
+
if (hasAgents || hasPrompts || hasInstructions) return 'vscode-copilot';
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function readSkillFrontmatter(skillDir) {
|
|
68
|
+
const skillPath = path.join(skillDir, 'SKILL.md');
|
|
69
|
+
const raw = await fs.readFile(skillPath, 'utf8');
|
|
70
|
+
const match = raw.match(/^---\n([\s\S]*?)\n---/);
|
|
71
|
+
if (!match) return null;
|
|
72
|
+
const yaml = match[1];
|
|
73
|
+
const out = {};
|
|
74
|
+
// Extremely small YAML subset parser: key: "value" / key: value
|
|
75
|
+
for (const line of yaml.split('\n')) {
|
|
76
|
+
const trimmed = line.trim();
|
|
77
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
78
|
+
const idx = trimmed.indexOf(':');
|
|
79
|
+
if (idx === -1) continue;
|
|
80
|
+
const key = trimmed.slice(0, idx).trim();
|
|
81
|
+
let val = trimmed.slice(idx + 1).trim();
|
|
82
|
+
// strip quotes
|
|
83
|
+
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
|
|
84
|
+
val = val.slice(1, -1);
|
|
85
|
+
}
|
|
86
|
+
out[key] = val;
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function loadPlatforms(skillDir) {
|
|
92
|
+
const platformsDir = path.join(skillDir, 'platforms');
|
|
93
|
+
const entries = await fs.readdir(platformsDir, { withFileTypes: true });
|
|
94
|
+
const platforms = [];
|
|
95
|
+
for (const e of entries) {
|
|
96
|
+
if (!e.isDirectory()) continue;
|
|
97
|
+
if (e.name.startsWith('_')) continue;
|
|
98
|
+
const manifestPath = path.join(platformsDir, e.name, 'manifest.json');
|
|
99
|
+
if (!(await pathExists(manifestPath))) continue;
|
|
100
|
+
try {
|
|
101
|
+
const manifest = JSON.parse(await fs.readFile(manifestPath, 'utf8'));
|
|
102
|
+
platforms.push({
|
|
103
|
+
platformId: manifest.platformId ?? e.name,
|
|
104
|
+
displayName: manifest.displayName ?? e.name,
|
|
105
|
+
adapterVersion: manifest.adapterVersion ?? null,
|
|
106
|
+
hasTemplates: await isDirectory(path.join(platformsDir, e.name, 'templates')),
|
|
107
|
+
});
|
|
108
|
+
} catch {
|
|
109
|
+
// ignore malformed platform manifests
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
platforms.sort((a, b) => a.displayName.localeCompare(b.displayName));
|
|
113
|
+
return platforms;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function resolveThisDir() {
|
|
117
|
+
return path.dirname(fileURLToPath(import.meta.url));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function resolvePayloadSkillDir() {
|
|
121
|
+
// Priority:
|
|
122
|
+
// 1) packaged payload/...
|
|
123
|
+
// 2) repo dev checkout: ../../skill/...
|
|
124
|
+
const thisDir = resolveThisDir();
|
|
125
|
+
const packaged = path.resolve(thisDir, '..', 'payload', SKILL_ID);
|
|
126
|
+
if (await isDirectory(packaged)) return packaged;
|
|
127
|
+
const dev = path.resolve(thisDir, '..', '..', '..', 'skill', SKILL_ID);
|
|
128
|
+
if (await isDirectory(dev)) return dev;
|
|
129
|
+
throw new Error('APS payload not found (payload directory missing).');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export async function ensureDir(p) {
|
|
133
|
+
await fs.mkdir(p, { recursive: true });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export async function removeDir(p) {
|
|
137
|
+
await fs.rm(p, { recursive: true, force: true });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export async function copyDir(src, dst) {
|
|
141
|
+
// Node 18+ supports fs.cp
|
|
142
|
+
await fs.cp(src, dst, {
|
|
143
|
+
recursive: true,
|
|
144
|
+
force: true,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export async function copyTemplates(templatesDir, workspaceRoot, { force }) {
|
|
149
|
+
// Merge-copy everything from templatesDir into workspaceRoot.
|
|
150
|
+
// If force=false, we will not overwrite existing files.
|
|
151
|
+
// We implement this with a manual tree walk to make overwrite policy explicit.
|
|
152
|
+
async function walk(rel) {
|
|
153
|
+
const abs = path.join(templatesDir, rel);
|
|
154
|
+
const st = await fs.stat(abs);
|
|
155
|
+
if (st.isDirectory()) {
|
|
156
|
+
const entries = await fs.readdir(abs);
|
|
157
|
+
for (const ent of entries) await walk(path.join(rel, ent));
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
// file
|
|
161
|
+
const dest = path.join(workspaceRoot, rel);
|
|
162
|
+
await ensureDir(path.dirname(dest));
|
|
163
|
+
if (!force && (await pathExists(dest))) {
|
|
164
|
+
return { skipped: [rel], copied: [] };
|
|
165
|
+
}
|
|
166
|
+
await fs.copyFile(abs, dest);
|
|
167
|
+
return { skipped: [], copied: [rel] };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const summary = { copied: [], skipped: [] };
|
|
171
|
+
const entries = await fs.readdir(templatesDir);
|
|
172
|
+
for (const ent of entries) {
|
|
173
|
+
const res = await walk(ent);
|
|
174
|
+
if (!res) continue;
|
|
175
|
+
summary.copied.push(...res.copied);
|
|
176
|
+
summary.skipped.push(...res.skipped);
|
|
177
|
+
}
|
|
178
|
+
return summary;
|
|
179
|
+
}
|